From 742c3df67e4d840431a355eb354e13575dddc7b0 Mon Sep 17 00:00:00 2001 From: BexTuychiev Date: Fri, 6 Dec 2024 23:05:18 +0500 Subject: [PATCH 01/52] Add assets for the Automated Amazon Price Tracking article --- .../.github/workflows/check_prices.yml | 33 + examples/automated_price_tracking/.gitignore | 1 + .../automated_price_tracking/check_prices.py | 49 + examples/automated_price_tracking/database.py | 134 ++ .../automated_price_tracking/notifications.py | 36 + .../automated_price_tracking/requirements.txt | 9 + examples/automated_price_tracking/scraper.py | 38 + examples/automated_price_tracking/ui.py | 86 + examples/automated_price_tracking/utils.py | 28 + .../amazon-price-tracking/images/actions.png | Bin 0 -> 132889 bytes .../amazon-price-tracking/images/alert.png | Bin 0 -> 22980 bytes .../amazon-price-tracking/images/discord.png | Bin 0 -> 206048 bytes .../amazon-price-tracking/images/finished.png | Bin 0 -> 259992 bytes .../images/linechart.png | Bin 0 -> 254643 bytes .../images/new-alert.png | Bin 0 -> 33908 bytes .../images/new-server.png | Bin 0 -> 103568 bytes .../images/sneak-peek.png | Bin 0 -> 268070 bytes .../images/supabase_connect.png | Bin 0 -> 163039 bytes .../amazon-price-tracking/images/webhook.png | Bin 0 -> 90072 bytes .../amazon-price-tracking/notebook.ipynb | 1753 +++++++++++++++++ .../amazon-price-tracking/notebook.md | 1237 ++++++++++++ 21 files changed, 3404 insertions(+) create mode 100644 examples/automated_price_tracking/.github/workflows/check_prices.yml create mode 100644 examples/automated_price_tracking/.gitignore create mode 100644 examples/automated_price_tracking/check_prices.py create mode 100644 examples/automated_price_tracking/database.py create mode 100644 examples/automated_price_tracking/notifications.py create mode 100644 examples/automated_price_tracking/requirements.txt create mode 100644 examples/automated_price_tracking/scraper.py create mode 100644 examples/automated_price_tracking/ui.py create mode 100644 examples/automated_price_tracking/utils.py create mode 100644 examples/blog-articles/amazon-price-tracking/images/actions.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/alert.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/discord.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/finished.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/linechart.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/new-alert.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/new-server.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/sneak-peek.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/supabase_connect.png create mode 100644 examples/blog-articles/amazon-price-tracking/images/webhook.png create mode 100644 examples/blog-articles/amazon-price-tracking/notebook.ipynb create mode 100644 examples/blog-articles/amazon-price-tracking/notebook.md diff --git a/examples/automated_price_tracking/.github/workflows/check_prices.yml b/examples/automated_price_tracking/.github/workflows/check_prices.yml new file mode 100644 index 00000000..5bd0e671 --- /dev/null +++ b/examples/automated_price_tracking/.github/workflows/check_prices.yml @@ -0,0 +1,33 @@ +name: Price Check + +on: + schedule: + # Runs every 6 hours + - cron: "0 0,6,12,18 * * *" + workflow_dispatch: # Allows manual triggering + +jobs: + check-prices: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run price checker + env: + FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} + POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: python check_prices.py diff --git a/examples/automated_price_tracking/.gitignore b/examples/automated_price_tracking/.gitignore new file mode 100644 index 00000000..1d17dae1 --- /dev/null +++ b/examples/automated_price_tracking/.gitignore @@ -0,0 +1 @@ +.venv diff --git a/examples/automated_price_tracking/check_prices.py b/examples/automated_price_tracking/check_prices.py new file mode 100644 index 00000000..33a48843 --- /dev/null +++ b/examples/automated_price_tracking/check_prices.py @@ -0,0 +1,49 @@ +import os +import asyncio +from database import Database +from dotenv import load_dotenv +from firecrawl import FirecrawlApp +from scraper import scrape_product +from notifications import send_price_alert + +load_dotenv() + +db = Database(os.getenv("POSTGRES_URL")) +app = FirecrawlApp() + +# Threshold percentage for price drop alerts (e.g., 5% = 0.05) +PRICE_DROP_THRESHOLD = 0.05 + + +async def check_prices(): + products = db.get_all_products() + product_urls = set(product.url for product in products) + + for product_url in product_urls: + # Get the price history + price_history = db.get_price_history(product_url) + if not price_history: + continue + + # Get the earliest recorded price + earliest_price = price_history[-1].price + + # Retrieve updated product data + updated_product = scrape_product(product_url) + current_price = updated_product["price"] + + # Add the price to the database + db.add_price(updated_product) + print(f"Added new price entry for {updated_product['name']}") + + # Check if price dropped below threshold + if earliest_price > 0: # Avoid division by zero + price_drop = (earliest_price - current_price) / earliest_price + if price_drop >= PRICE_DROP_THRESHOLD: + await send_price_alert( + updated_product["name"], earliest_price, current_price, product_url + ) + + +if __name__ == "__main__": + asyncio.run(check_prices()) diff --git a/examples/automated_price_tracking/database.py b/examples/automated_price_tracking/database.py new file mode 100644 index 00000000..2aec92a8 --- /dev/null +++ b/examples/automated_price_tracking/database.py @@ -0,0 +1,134 @@ +from sqlalchemy import create_engine, Column, String, Float, DateTime, ForeignKey +from sqlalchemy.orm import sessionmaker, relationship, declarative_base +from datetime import datetime + +Base = declarative_base() + + +class Product(Base): + __tablename__ = "products" + + url = Column(String, primary_key=True) + prices = relationship( + "PriceHistory", back_populates="product", cascade="all, delete-orphan" + ) + + +class PriceHistory(Base): + __tablename__ = "price_histories" + + id = Column(String, primary_key=True) + product_url = Column(String, ForeignKey("products.url")) + name = Column(String, nullable=False) + price = Column(Float, nullable=False) + currency = Column(String, nullable=False) + main_image_url = Column(String) + timestamp = Column(DateTime, nullable=False) + product = relationship("Product", back_populates="prices") + + +class Database: + def __init__(self, connection_string): + self.engine = create_engine(connection_string) + Base.metadata.create_all(self.engine) + self.Session = sessionmaker(bind=self.engine) + + def add_product(self, url): + session = self.Session() + try: + # Create the product entry + product = Product(url=url) + session.merge(product) # merge will update if exists, insert if not + session.commit() + finally: + session.close() + + def product_exists(self, url): + session = self.Session() + try: + return session.query(Product).filter(Product.url == url).first() is not None + finally: + session.close() + + def add_price(self, product_data): + session = self.Session() + try: + # First ensure the product exists + if not self.product_exists(product_data["url"]): + # Create the product if it doesn't exist + product = Product(url=product_data["url"]) + session.add(product) + session.flush() # Flush to ensure the product is created before adding price + + # Convert timestamp string to datetime if it's a string + timestamp = product_data["timestamp"] + if isinstance(timestamp, str): + timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H-%M") + + price_history = PriceHistory( + id=f"{product_data['url']}_{timestamp.strftime('%Y%m%d%H%M%S')}", + product_url=product_data["url"], + name=product_data["name"], + price=product_data["price"], + currency=product_data["currency"], + main_image_url=product_data["main_image_url"], + timestamp=timestamp, + ) + session.add(price_history) + session.commit() + finally: + session.close() + + def get_all_products(self): + session = self.Session() + try: + return session.query(Product).all() + finally: + session.close() + + def get_price_history(self, url): + """Get price history for a product""" + session = self.Session() + try: + return ( + session.query(PriceHistory) + .filter(PriceHistory.product_url == url) + .order_by(PriceHistory.timestamp.desc()) + .all() + ) + finally: + session.close() + + def remove_all_products(self): + session = self.Session() + try: + # First delete all price histories + session.query(PriceHistory).delete() + # Then delete all products + session.query(Product).delete() + session.commit() + finally: + session.close() + + # def remove_product(self, url): + # """Remove a product and its price history""" + # session = self.Session() + # try: + # product = session.query(Product).filter(Product.url == url).first() + # if product: + # session.delete( + # product + # ) # This will also delete associated price history due to cascade + # session.commit() + # finally: + # session.close() + + +if __name__ == "__main__": + from dotenv import load_dotenv + import os + + load_dotenv() + + db = Database(os.getenv("POSTGRES_URL")) + db.remove_all_products() diff --git a/examples/automated_price_tracking/notifications.py b/examples/automated_price_tracking/notifications.py new file mode 100644 index 00000000..2837fb70 --- /dev/null +++ b/examples/automated_price_tracking/notifications.py @@ -0,0 +1,36 @@ +from dotenv import load_dotenv +import os +import aiohttp +import asyncio + +load_dotenv() + + +async def send_price_alert( + product_name: str, old_price: float, new_price: float, url: str +): + """Send a price drop alert to Discord""" + drop_percentage = ((old_price - new_price) / old_price) * 100 + + message = { + "embeds": [ + { + "title": "Price Drop Alert! 🎉", + "description": f"**{product_name}**\nPrice dropped by {drop_percentage:.1f}%!\n" + f"Old price: ${old_price:.2f}\n" + f"New price: ${new_price:.2f}\n" + f"[View Product]({url})", + "color": 3066993, + } + ] + } + + try: + async with aiohttp.ClientSession() as session: + await session.post(os.getenv("DISCORD_WEBHOOK_URL"), json=message) + except Exception as e: + print(f"Error sending Discord notification: {e}") + + +if __name__ == "__main__": + asyncio.run(send_price_alert("Test Product", 100, 90, "https://www.google.com")) diff --git a/examples/automated_price_tracking/requirements.txt b/examples/automated_price_tracking/requirements.txt new file mode 100644 index 00000000..52f0541b --- /dev/null +++ b/examples/automated_price_tracking/requirements.txt @@ -0,0 +1,9 @@ +streamlit +firecrawl-py +pydantic +psycopg2-binary +python-dotenv +sqlalchemy==2.0.35 +pandas +plotly +aiohttp \ No newline at end of file diff --git a/examples/automated_price_tracking/scraper.py b/examples/automated_price_tracking/scraper.py new file mode 100644 index 00000000..fc06b73e --- /dev/null +++ b/examples/automated_price_tracking/scraper.py @@ -0,0 +1,38 @@ +from firecrawl import FirecrawlApp +from pydantic import BaseModel, Field +from datetime import datetime +from dotenv import load_dotenv + +load_dotenv() +app = FirecrawlApp() + + +class Product(BaseModel): + """Schema for creating a new product""" + + url: str = Field(description="The URL of the product") + name: str = Field(description="The product name/title") + price: float = Field(description="The current price of the product") + currency: str = Field(description="Currency code (USD, EUR, etc)") + main_image_url: str = Field(description="The URL of the main image of the product") + + +def scrape_product(url: str): + extracted_data = app.scrape_url( + url, + params={ + "formats": ["extract"], + "extract": {"schema": Product.model_json_schema()}, + }, + ) + + # Add the scraping date to the extracted data + extracted_data["extract"]["timestamp"] = datetime.utcnow() + + return extracted_data["extract"] + + +if __name__ == "__main__": + product = "https://www.amazon.com/gp/product/B002U21ZZK/" + + print(scrape_product(product)) diff --git a/examples/automated_price_tracking/ui.py b/examples/automated_price_tracking/ui.py new file mode 100644 index 00000000..11969897 --- /dev/null +++ b/examples/automated_price_tracking/ui.py @@ -0,0 +1,86 @@ +import os +import streamlit as st +import pandas as pd +import plotly.express as px + +from utils import is_valid_url +from database import Database +from dotenv import load_dotenv +from scraper import scrape_product + +load_dotenv() + +st.set_page_config(page_title="Price Tracker", page_icon="📊", layout="wide") + +with st.spinner("Loading database..."): + db = Database(os.getenv("POSTGRES_URL")) + + +# Set up sidebar +with st.sidebar: + st.title("Add New Product") + product_url = st.text_input("Product URL") + add_button = st.button("Add Product") + + if add_button: + if not product_url: + st.error("Please enter a product URL") + elif not is_valid_url(product_url): + st.error("Please enter a valid URL") + else: + db.add_product(product_url) + with st.spinner("Added product to database. Scraping product data..."): + product_data = scrape_product(product_url) + db.add_price(product_data) + st.success("Product is now being tracked!") + +# Main content +st.title("Price Tracker Dashboard") +st.markdown("## Tracked Products") + +# Get all products and their price histories +products = db.get_all_products() + +# Create a card for each product +for product in products: + price_history = db.get_price_history(product.url) + if price_history: + # Create DataFrame for plotting + df = pd.DataFrame( + [ + {"timestamp": ph.timestamp, "price": ph.price, "name": ph.name} + for ph in price_history + ] + ) + + # Create a card-like container for each product + with st.expander(df["name"][0], expanded=False): + st.markdown("---") + col1, col2 = st.columns([1, 3]) + + with col1: + if price_history[0].main_image_url: + st.image(price_history[0].main_image_url, width=200) + st.metric( + label="Current Price", + value=f"{price_history[0].price} {price_history[0].currency}", + ) + + with col2: + # Create price history plot + fig = px.line( + df, + x="timestamp", + y="price", + title=None, + ) + fig.update_layout( + xaxis_title=None, + yaxis_title="Price ($)", + showlegend=False, + margin=dict(l=0, r=0, t=0, b=0), + height=300, + ) + fig.update_xaxes(tickformat="%Y-%m-%d %H:%M", tickangle=45) + fig.update_yaxes(tickprefix="$", tickformat=".2f") + st.plotly_chart(fig, use_container_width=True) diff --git a/examples/automated_price_tracking/utils.py b/examples/automated_price_tracking/utils.py new file mode 100644 index 00000000..c7af0a94 --- /dev/null +++ b/examples/automated_price_tracking/utils.py @@ -0,0 +1,28 @@ +from urllib.parse import urlparse +import re + + +def is_valid_url(url: str) -> bool: + try: + # Parse the URL + result = urlparse(url) + + # Check if scheme and netloc are present + if not all([result.scheme, result.netloc]): + return False + + # Check if scheme is http or https + if result.scheme not in ["http", "https"]: + return False + + # Basic regex pattern for domain validation + domain_pattern = ( + r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$" + ) + if not re.match(domain_pattern, result.netloc): + return False + + return True + + except Exception: + return False diff --git a/examples/blog-articles/amazon-price-tracking/images/actions.png b/examples/blog-articles/amazon-price-tracking/images/actions.png new file mode 100644 index 0000000000000000000000000000000000000000..875241776c26f876461bfd147eab751bd06d2c8f GIT binary patch literal 132889 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 LaINU--N*kAgG7rM literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/alert.png b/examples/blog-articles/amazon-price-tracking/images/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..34bb52345347637c2bd84aecf6d74c99cfba555c GIT binary patch literal 22980 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 literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/discord.png b/examples/blog-articles/amazon-price-tracking/images/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..aa845f6847c8c6f494c180492d42bcc1e03e5541 GIT binary patch literal 206048 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

^>zGX;x*R`TpOkESPagKwv~b+|PL3MAc=jpz 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ÐSUYRJF3yYMC!y>ogcG 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 literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/finished.png b/examples/blog-articles/amazon-price-tracking/images/finished.png new file mode 100644 index 0000000000000000000000000000000000000000..f172000b824ddf8424d4fb1e94cb7f782488c4a8 GIT binary patch literal 259992 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 z zR_u~5G#5O`Gkg;7c!*IGk{dt@Toq@BGd88A;3CKuHZ74*7T%G4=p(P3j-&Lg-ccx3 z$TwP()exhttAmT^ZNrSR z(OG*&X68Xq=~ujGo^f~70~UpPOG#)=SpZiA&2Pr;IR9!3>2D=U@X=Pc~k+Qlz- zyUKqUrWFr7q+GZtXzEMwxq|~u)0;!N{Ood-`ChP2+Qv1i#engqw*yH87T*x?;Jf4H zq`38nnhmwZ#p6<Y&QO;V22eU!#L07HfeW7o)$00QH3jK+~k)-{~xTz7%W?7#7 zHc~}&uA!?%u^iM{t`inDPZ>s%x4Ha!T28zv`j6A=l>>HN_N3SrBH~nohSGKYcYsEq zKQX-S@^Esxdg=Q5mvo0N+lVZm3x|{1WyQ!AZ8)H|EP>dEdC;BS&U)F)Yhv!}vpgY3 z?zd)oDB=GeMJ=W1^NJ3C<ubafvD*FC?gp zl3r1m(wR0R0d^;7kcJ3yb4g$Hxe zzGZaH%%-lR<2k^AvRpuu%{(83@KLi3L9Y-V;r`g<^Veo%bM>e3>1Th>D5E`coBiq+ zed4zSu}&N^pysnE%KV3oqt{#nfmsF_4ZE)A8-e5MzUyMW%6TTXj>waQ(zjF##L0pU zq2?b5Qj_FpzVa^}owV>=)BN*PU*SuBZ}S@acSN!q-S zk~=Sh{)|wIl`+{W35@m_Hu`->Avn63>Wy$jgJ#N8IKd5we?hoEZ-W*$^pNHJX3fH&&p4`RU4SvSD0ws~H#?)XHt$7I>&RJ7{n@ zy_21PL!YDN?RUZ-dir}&Sw~lpO95X6`%R)T^}x}9=t_ReStmH%u@w|6bmJX!{@+;y z{|k12hCso=RORfG-A<18LXn9(GVDL7k!Ogzw>^2h_RZ=|E6z=zqA z?rsaIM)3sYmDh}A$AZkp->bNuyq?*;0>#c7Vk)#+8%)7w8TWy@B=Fx#C}k8SJa1QR0GTw+jm?=fxXI<#a`RT`avfT5fA@$uLhQ%UnwA}Ugu7i}c%uLiwCj3M) zssX$=+7VJn#=luc&<~MJ=$77roMP7wtxm*r#8DB4(xJgK5>1x})TJwV?5VgFHg?ga z;{gT~5xNt-p$SsZqMS(RLoXfXK^Zh zK{7{6+uXeh8*7INx{p7sPQ6_yCGyHjN~G{EQyW*^1!3o7#Sqj`YXE^cD1Y>uD?K?U zQnjX%#NWU<#f$Odj*U$gp>wxx#_}G+ z@yY-3gb9hCuabqFZN<(osTfv}KW#`Ee14%3I00v~Q-HdQe`VaYY6o`2phE7p_^39! zM4iW6>0Y-8L3vGc*LBp4$8FC>Rh8s-`)PuKO_dTabL$n|c5Kw-%$9iuq$l9R@ege@ zkypp#6O5H`pi{j#m-x;*pJ?}Wd&FXdq-^4!?@A1oRm!Fq=*Y)jPyp*kP_ZWEM1c9! z4|wr;oB&c)Ujs!=O-baS0cD0ReM6rbprDGdm}5t$e$&cR%9`J$x+Vw%hgDLWmHQx> zHw$zoBCP!!OZ7gkZ^$q5>ASq~oMs8y%J=wXg06~G808ZK1M+->|5+A7<#|uxaTI>ccop5J}=%GjCn&Jt$VB0IVNmg`F@V=p#<#1u7Isy1XD!Usd;#2O0g0TVN zw)&jOO0r-bfA2%eS^LE(jv|TYNA#(Uz`ohGGozkBF+Vw)oemDV{~tP;8_LE1389CX z`8UaKJ|ZjVHkcs1lfre~MEHTbZ^!gh(^X>(d>Ws59eo?2QrWw#BeAaV_Q_+Qt0CW? zZ-K?=RoAd6@3ReB+uZz3+qY!}542W{w5i0LqfpVGQIU6y`tq&?VQG{7JLd?p8$6Y#DR+7X_XLh+kQM(Rr#@8(FoRX$$UmQR7jGEY5YwI5OYa zm8CqbPrmWYC`rVW#~F|Wx%WgsycygZgK=)+*|jAp9!XU=q$Hc$)xpENMen0tH2)k1 zLCg?2E49(xWUqt!;s*0A8FWG}sq{I=(EXBt=3hWPv^gbcpAUd$gnONV%h|cE!S$Zb zYd?38+irmZTo{=e7!rt2pCMh%SB8?Vg2Vms-F84UucXy>DcoBLo&KSrqw*vF+vXe5 zJ4JLNfrwNDF2^;&G%jnBt;KC;`G>31Drokq&R&KKWmznT9A{sY z6ecx<9e_nexwE^ew}Vl;*PUfF^T7#%lhDngC;am!g6>6u-XjZ|nx_i&yr;BBAzm=e zS>0O>bG!bOdq9hmKi3Ou^Z!pS)Bo9=ySrfj-kKfWBK)_rp}Pwo77f30jyhJqiVV+} z6w|0t8Pt7ZV_0!Geq67{wsTdP{i?Uj+S4aWa@^`VGC1$|pt~-k0Iz+qX084n8L7^e zYAMZ&EBjs;{V|RTB7p7alFfqF1DmG~K8$UI>f*nSVtxO(fEF>1p57#;++J$<$?e-x z+7Ulnk-GAVBwcnrIU(+Mni`Vdx!;dNN{UX{V$|PXUecfqJH^q*!V?O5US5h5L9YG33_Zl*D0s6N}YANib8r0?qCFbi53Z2;VFia=Gj( zdbI!JZzSd+1|{%TH(4L}|MivyI)rxL2%dsVr;A#3{>oKG2uLzt7WON0w-7Y`p{v#^ zPE;tEgi)BpiU$vmiw6Yuis{BBfD~HW+H}%R)-qEwAF_{hzK?sYpP|2YjfXrAEFSE& zoLvf?j_L0S?8&u&$`?gw>A4O9g&xSO9x%6q8yP6^%^ch3AQ)?Y`UO7*E83Ai^gN^S zi*rcyR?x`y{y%hmWl)=K*KLtPumZu|U5mQ}FIrrRySo%EF2O18THGn_?oiy_io3(f z^Ss~p&iQf9&rI%Ql9_#7Th`iZ?U@b0W+yP(nh9Bp*`>FM8T0n?JX-SD+5H@)enU3J$HTKIRSA}910kuDtEbwv< zr+2F8yDgB1*o1mRGKT~exuQyd@#Q&Q?%`t1GUev-*HC&Dpts@F1J{{|y(1IWUKk^b zPFKW;7Ol^g*CO}lmJ=!XP0_c*w{73?^3&|6z;vjk-^isp$jHZTl;Pa#Y%mDD2ZZ7Dwi_L{Rud)fAs3|#JH5knffy>H9d#&AG^=Ty~zgw z7zi?!^~H!gHDrcjdm*xS!-St|T!C$U^Is-kkdPADjl8H1YQYZ##l@B@9ZW5**5Li| ztONuDtkbM$VxedRd%v)DuNdQvGx_D6zo8%zvK%xe04*#gG$K^+kN>{Rf`xUVl2v~) zzYV1^jIu)c_sz`>kcg?$i;lzVR=-~`>eiE5*~QEC66{CoO~wIdZV!~Z`k0YZ4uyri1lQaj>Nzp1`k}#RGXKCi z3r!`dwGx%4xk?nq_qYy?`|NQr#aqlbmr?N#uZ+ zl*c%#zo_<$(q<2^iITt05^7v7)wBh%f;0PKhIjWbjSo2;le|AgZ8=OQ$yRS`>t>MH z$7ML8J)6J3{6NI-0`i9rzKBiIU~%)!e*P-<{|+fk=yz_w z3sysRM(SvzE$4p&?xGQtn0~`2REdtZr&WyB(~2Azm}kL zQ`%D!>YQYW%ahq-)P_vYsiHPb|C%KA_?r0nDGExH&5y{LK-;Oq-GGC?n7k&}CROPy z`J=B7F5x_EQ!QJ4mo<^hhdKq&bhD@v+pJ1#tTfqDZ1mp-B**7Cd^-ht%2n^qnLUX_ zX}x_8fvz421~gZOkg8vxp0gkQgYZ!6lCx!MSS+2hP~D>6@zhUuomC@ad7PZ}73hg} zwNLViR~1F$AS5i0?ct|ePm;qHdK z{pe1--&iF)DyO<;`$v*;R*!dd?iI*H*v_qm1JS3P9wx0jjkw5nY= zuOY}iy^Es{4+)La<KxB?F3 z>J?EEbMzlCFAGSN>eO|CJlOHj#H(#ih~SR*G_vj9i{AU!>2h6TO_D51H-*x#@rHLO zdv|Tp`t*|}N3)_khs6{!88WZNKbO-`JK!fZ+4uW1UEo08h>955 z#V&Cf#UBzMu&2PK(GiGbX+j`WBc65OE=HQ zaL{HaNJLBERpX+Bn*owP70IDTwvRgbOZ|e!SU9(`xDZgwy7@}r>TGqxK1AVo$cbNE zO6|g0Pw{d$+#gAc3M}5@Fl{QB+618iuXtJ?v^eH%@;r1VdpJmT zgQ+ayv9(V6CWor8dU2{OCx50+kn>@oM>EoZkB%s@{=}uEgatKO^z{pgD?5?*{@g($ zzyPPD$n9fPca6*M45bC&FA1wuc#&>m@3nQc!RiF){v%p!K zn|XQ=g_s~RnN8~YKZp}#Rp_F|NklkSGL8|vY}y*-W|NI8Xp@^RB{)1@p=YvgGTm#T_Q{gI)-sTWJH@SIw?xR_9bn3!_W z|3natnc$2|a|(A;PA$kCYw}b%ERHBvE)zamkJ$14qg0^z*>=svi#)nM>uUJy_~Nj* zDaWcu25U5{y>R^(3eoyF?J;8tjXs9-}hThiix@xhYjDh z#-P}mDx^?z!hi3oKq_nt0IC;i~LLa)@ ze$r~ix(R(p*JFjnqEFs7_8r4#?HTz74k{2A@NmeU2RTZI{jLjKA3${*qcR}jNXw6DHb4R~(Nc58jXuioP!w9z!yd zM5Z?S!+yFUEoQ1h?E#~=<<72rz>jd$>SmwfpF^Pp?g=&Ln|PGY9iDbmU4CZysNQX$ z&2c4bl^+SE;nMM9LMfbHg5hz>s=;#8JT(!el;f!0}m15xdS#?V4UiYhc!DrbZlaxUJ#qXeA z3*p3W+X6@^vE<4AN-jaJ$*o-=5I?CKX~tF z8~)qlyqN){r{5<-Tn=ToGH#9aJfy5z%Ed<$CwjLlCz2DL;HcOsMETxkRCboa&x4kF zri@BSUtYasl%@L`5T>UuHOCFkf;U+55y=zRp0!k^O_cmE!meYZMm8I8PJ=-KF0{Hm zL0uyH%Wzl{aYNo=k&zCwbH@+lB z-N3b01#-@)+~M!uO;JEvwLBow`P+qAHy;HjihJcCDcNV@^ONV)QpBl0LqU)ZFArk( z*5;cuch^eEan`f*W%~?>M{B$1RiSDcv@>5wDG;V`@b*pByN@iznNErJ!$(B8*oOvyYB3VRb2q%5 zZc7Uf{}eI=K-XPkaDd6dw90+t;KO)}5~U7-B8As9>@QmnPw6EoSN-H|_j;%yE*g?% zf0#`8*kqJ&H%ajQU4>736Rl++-L`&E2o|}Y$t)%&@{b(0fWHXzx@~PT-c8<970-ax zX4C%KJ)NynS@ke4mnrfuMTfEwrGS_FbAi4h+tFfio*#cERmD3ye5y^RZQ8Eg;N{*5 z7;1|rE)4agEK;Dj^A++h*f||{o@@mcs)-vZMu>ovECkndD_2=`ri;eR8f6gQ@FkBi?aBUn#?EGLx#Ira&!R)s2!(p|JX!6s8?StX8cLtAJ z`p56|NDt~xpDrjmm8yTBB4Qzmm(<45stZ6Dj$)b`G~kIs`6Fw868X^?Ob>-!DTt>^ z#X1Sf7PM@g`&7o18WeyKessy2{#yUdzj#JtNw5iyCR#tV${rU&9R!mkEA+BcG1O^g zm4}W!bKCr248Xxr!(aAYZE^?a&aX5YO`MG53Rk%iGUIhVhf1wLvn9CVZ5|GN2^9nl4U(hjDnvy_&f76;PXd= z+bdm*`C1_?MBok4B=2MQxbAWL!<#;8Dt7lm-HYFQx8Q7r28j@I-aa!W$lvNirsPH6 zfUt>&JGI?L3zDCiK+wkn(esW#$NgEyW%AShfvAIr{tASOND~@VItRnprnzvHUlTjQ zIPegYbvD*VHa%@hG$#j(k^)_8XGb=hCqksROCGluYO%iFV$~^Y=Zj0Dq>$Y(R%4UP z`>eQf9vv3 zgnR9TTO3%6Gm)7D(@L7Gwz{LD5O9g7bJE1Op;&G>D&Jdpb7U5IhniGYE#RK)`3(;1_ov(UFkX_V$+N+|Tt5c7t?RBEHJ}^>kx34oS-BasKLPTf*V$DDdv5Oh_ZL8ya4w@aqf_^85-nwBW z=5QeG>}(H1rwDlmpDVMd0ABuS?OxC^Zo~fPntmani0v{svR79njGu zVYg{{`3cyT^pEZ>hzG~wEbkCwGTtFSA=7q`8pJcX*(}#*z>Z3qEZOC|43om?HRkgL zq~dAaSG$gWT_#_XWM!x62?#VCm-Zh|3<#?x?;Azb84z?iQ-;)Bs}l{)4kjCFC+ohr zEcKP)jP4w^Apz0~u@~s^yD_4eeY&BT`%JLPyj%U-UK67V*}P&@J)9=7xQNJz1*a+q zXDfln+~-|`+kr*nd^=|WwBApT^d4FuEe{qcAYGCG>>MIx44?ba4|xKvo#jN2?G%z= zFqY`nb{qhi>=SZ963=y{DpAL)+h<8+Kk!Jn5PXwZScg&_i;sJzd1MJR2rRdDQN(L4+oA#$)jWOk z!yu{;w7~OVJHFoY(50*+XH24~e83&RHEi4x6OT^vlx`-~+5lCIr$d z?Lt179%C63n?L*iiD*AR*fdVd0G5uF($}+1)HnJ35pbuN-FlO#PGd3$uuga7x5cCp zMC3)-2toob$weC)2YA0s4?-r}nMnjxe(e3B!U_HtzZ5e2JhaIX4L`dKk(Mfs8VoG3 zJ3NjguC{&SbPxV8|K3I{0mj@jclA}AVxAJhsnl&xyUR~N8|gZkreuIfI?(Vk%FrPv zK^js>5{QK>7xcQD9#HSdHhYhDSFvS=y@6K++i%ps7#(rjEV5?ox%X(*nO}RwdM~L# zvO*_C4jb*8d*m#`Kh^p#WT-^Z%MGH7&Yn=#r!$o0JxVLrM-$)Jn3nG<$0Jo5ox~NX zebB=?&wt9sRr#Lw$c5`CzSkg)7~J#s#<(h&kuGi-Uzu}vqKu{{vikhxSPlCUBFs*% z{kuFMnq}71!WkFzk{Y}zah$^l1Tr#%6VV=g4~bUU8Yw_WnjQIF?*VVN8krFf%MSiX5Wqy3of9I^X<}~21M?ECs;pK+sW#)r1Z|Yii{9fba z^WX~%n6Oop9VX9KC-roaD8Rb_rqV;*31y+vU$_P1F9@mUm}?Z|Ad)2p`9M zl%SGWSgH=RiqWN@nO-Sd}lIyb)Ft*tw&*?ABPJ^3PfzCX4^?sgrF0h0+f0qhST>1)lPe6n9R!lgM9qwt9@&SxNN@riwZY~>&Vvg za^hQglu;>c#F)5p{7n2H%U;+0L(!j%_%cr9sY%+B1?Cbe-WdJ`W6;H_m$rR0GC2(g zHJ;>Bykvv>5TOq3!>xMDnfkm@3m}@2apG_W0ugGCw}XBUni57}4Uz=*A3%ol!9>2) zBym_oL{ElRonth81aF;ru0d|VH*=1wkOR@#Q`!)ld&wQ@$(_LP^RHx1KYH-k4>qUR z#9aj7b;lH&zhA?Z&Yh$= zSE|D}F`w_w-^Y(283QKHO6~FGD=+MzR#VRfQ4<>TaC@R7jxJxb;(X=<3JYz?~=K@G((U%;Kr~2CL zWLnXx+cFx=LrM@Quk?d#F$*@ySqos4+*vh@I*b?YaXgNo`P-ySeXLTAB3AceXK^bo zyGlmf`-Ez7hJA6`z5w4p2)2%~k721Rhc3)#Gx=E2R6a{b zTnQZ27h;;0Zm}PBXuQKSpU({ES_F%=qqNYKbIdnu#=b@zy(q3ke>*U-k=`0vIjJa- zWMup{9NQp6(}6i8PYuU>ooO>=j;{Y}T=BQ*X4{vf$_m{UcOriwpA}N3nLu^h()gfZ zqnZt5VOd-(635jXbW6)iz=d1cd!{SN1lRSqY;KPYrN-GQMz!=HvBSM`*_Xfkd@e6Ip=h%>j7pb5>QLGKG7 zM8RZfCf^kc9N zP8gF;g2zkQXH0&Q&z|+V=$b>aVbd|AeqA)*G;%?Dl;8={E^f4=VT6n8Yq6JCYf7?W zHv^vL)WJo=L^MaBq`*A7>i7kjc0e2{`BA*_DJ9XQ6Y{Rp(~V_?Z1MN&{WMQ0E8Rb~ia)Zm1uGn<38iy3c}4UZ@7umPHlzk7}STnO?WqjI#-`{r~d_=;j zf8H2-z`xBZt6EwaG4A#k5PT^v(`_19kQ%<1zNsuq>Ba9OQVqKEtoonSXHqP8^?pT_*xd`3qq&e=(|W^V9JouonB~_4+xP;+I)$-bc5^f54A9p@A+h+c`_ySYOxTUveW02n9h^AgLj!i zvbi&RdqermRNA6tlZh`3>0#NnOPZv2zZq>EpNmWKIhu9_ji&b4Zb-eqxi&}hJbVTl zjT9_3xzi*#n~`!{28I5Klk%GW`@W1y%EU=z$k=ksiGbj+wf#}x*@hD+>l-<~$u}%H zjU9 zqQnAW10z^vG7dlUNtgdwC&{0zbA44K03qzJm7c^-(VUzHwt> z_U+wnI*$vi7IwAs#A>a0Re%T3|MK7Yg)m@mHX1Cj@KVo}# z7^B&z+3F1u3LtYi!r*h%DI71MwcPh7?QZQ}s4EnMMu?gQaes1hfZl#NGgCbbG7}Yq z1}Tp{*-@Lt{*0Y&ki`+$$bsTx4J8k)wpeSy`ZF`*{Hi?%{kB1t1BiHc*DMpH`wv3i z)#?8u5vrF)F{`g97>QGdh=3{BNm5fY1zVXA4{zA04$J*a&y3#>pYV=Hn|P^M18p?- zLk6ECOul3rm`G=GYpK*XMaUM@^RC$cXP-)S;i~_1=E%S!BiBKl^f-AN*l?MTA{+$? zJlCS?_G(_6uZMW6pjRhtV{)6urF=EnzwL~DZ%mcDg`tFa%$+Tjts&e!w&Z-#O|wCk zC*o+wqVqWyt5hq4JGJip#3~PFJvrbtpW*cB>#4p@ktWvBcl3SH!4Gx_zDcij>JS(~ z1_pbZq%$>rjxgWkn$e<@_ssg{R1jfjkg5Bzjw}A-BhNXhkW?K#4ft-I8py&N65uS~ zVp8vwOU+KqCLNC(i)P;Yl>OVIS2t^l_wmr6sNu%HI-G$g`>A2P^@7EL>yjQ zXr+d*hph%5PNWcK*G5Sw*Qbd1wq!l6*R|3u+MV*UbWq<_8iXah-qZAz9UWIGx=$3g zEz(!DHVg~VHo?N$%uu>U%ZlM=U0VKEd$&K~()iap<~_r{L2& z!QsL$)Hw~#OQG=L0ThDmU#`Nr`sYH;mfx)^5&btvNhu{>pImEvcO*INmsM5TKH&-7 zTw3o>X8z)2lWu%W&zHP(?n0cUmiZL|B5fpxJRXHVX@!TeGZ5X|>}+0hGPt+w)Mh!^ zVzg)Fi!BhA(xf?7w06+O>rA%Q1G&1L7$8y4VY{rE{x(G>w$yZsV*AtH>YggpXjKQ} zP?J@@we$OonY85B=qsVdO0DtdcYWA`pSk~>)c=q~-qZmAsIL{;oE#Px0(>guOl06} zR*N$<{%JqKfPes{HV?<$s0%~GSMpN_X-d|65jNy)ViUuj6h(*aL$%$5gZDXxgB+Vw z5l<5RoL~ju7##MaRy~*WLyb#|N_)<|*Y|_N`ZwxJF zxAnTNs(d1qqTZlQXe@^_Wgr|%dZ48~i+F4FYs#evmVf%%JsdrB3I*v$Z#o_@ptl#g zHGR&3Ih-D~6j`%;vZJYqhE8%cI0@8NWEK#^i<_4*a3td|4DR<`=CTxrZS1~3-gP8G zfNjtG3rmLC#E4zjdYiMRQss^}A{a&Viri)<6p|1WzBb_?RM|^vxx*MgI3T{2Y7UKy zN!%v@cQ(B>5MkO$O3mm0JWh(&!-(qej6WPJ|G|M5If<(VH(OT~2TK->e}EC2{XYL! zg9Z&2e#-fOQUCan^#edN=RB&ClRYpXjaJzAs2 znb`%RVc5P*^>LxQvC(c{C4ux~k@E#qd=tSfz;$$iaO^o$$&g92jXhM!{p%8e{ilMyU6b(<@M=TjfV5IUe@`dUi!rImugC?-=)Obl!Oc?y zdrSQjMRrxE&wl!Ce>#P!+cSeb%);~*V|>f z>AxxgUhtqJavs=KXN~{27x0Vx(7LL{kwhMidYV{3aTJKU9=fz57qh@S2F3Gu7%0q+ zrshS=`uKc@;caQr9ie=L!( zVo?t!S0(a`?6=y(Npj?$v+&nQ=Ve{|jj{rQKnWZ}S2<4o5vqaSF+X>CmL-}s4H_vp zIk9nAvU=`L32@8X(6W8m@SID!7JGgS8%rr}s?QPu>A99M9&1v@kzrAhbdclROg<;~ zh{cPq=wKfotHOEMw3#T&h`PeRxA3RNO}8uBbJ$;#4|MXUnt=2=)^V^Cs??=_;b03W z5JA7D-fta%LYZB0GhSnNQ5^REFrBP9e|YP7V_aAH-erqq8SgV(1ob7E`SF1;&L&tg z3LhaEhCh*6lAd-|`G^K#v;QQLB>6oIViZVzCmN~lKyW*dUeCz3Gc&BeZ77F`h|Lg6 z8n5h(ZP#7H?#SwaCD}5U?WPG;Kh0(x1m3JPc8oEA@G)bp@P={YbmL!0p~!Ed#8!SG ztm)y0JtFxHta)YB@dq@8c?P9YduI+(1i^-5tyBB;jTgp|UIZ`w#3A4&96(}m`#Ju0 zfZee303DMCvHs0E<9T6Xx_sPnLRX*V^QH6%bA^q4Q5QO}aoq`d!S( zu*r3li%`G;;PZ6T53yygx%-t}-#~TYJ~T2{#_BSJ=2B!D071T1qq|Ur8TSvU1%<)3 zIc#^BS2(m`Me=DE_BVpImVX=>GMlXD%MAxB$oHI{FV(N%J2D@cY!$5yUMG)hoEPl4 z8ox<`vBycoD6GH;_KG5MavUM=U$kx>KK_Lymt?YP&$%y=xlR@vGRqc43T!;oA~mLEmp{kRyT*AkZxj5D!*r_=2TctVH~k zJI`V7uc>AmBkh}<6wow69*g|5C8DEK8QTuPVv~ zg(0k7TVd)s%bn+FMmrb=cX@vT)ZDli=)MCw#tJ-&9@vpPnS=Mob#lJf?O)taPB63L zF|M*5P=EBwTm;H=>qZ9D(joDtos63k=}86mv|4<#M=_gR1?R$~P4OK;SH!TDjFUEt z0sBQ(=*l?pqS90){Xq~Z!Ku@yc5Bh;DpS$rH?=_+f7@2VK%YUY?v%)v?nHfJrb}f!K-GC)sgD) zKSIw?`Hd~1=sL7J%#LUD%4g;+2ur4?S0D0ac8K7slC)+ctzx8ENFG0%G@6Hs>kgQ+ ztZ}Q(QkfDn0)+OqGX#;@<;2?Ka!GpaQdxc43caw@O9)QI9yd=o0F*8d$r5--dO-2_dA`5)t_&q!YqgG8DVB<8)~^zCqDPD`=NIMI`6q)A`O75Gs4&(1 z+|WojI;u5&Q5cu3-H0-yEGpdkd)D6C=_R?Q1~8$>0U)|k{|{ZZYXi+cW7~m?mkeX< zjP%!nuvQ>&Qb5d_kU&}4qF=l}&;(6-vAU58_3l(Kh<9T=I9UjZNoT&lWG4MJEU=o9zg|zM<*8Vn@py|pI`P@~whvq7T4#`Qee9ph z-k*X|*aD(ttI3HYp>nVX;a%H>&kj39>AHS15BNZwa)#qVhHNf5pdI7hZrhgVMXmHi zk5j$%3>+%S>aSa67Hx*ltLceO`sW{Z<21YEXn8JD2_L?wl1fS*mq}HACm>ukg$>&5!=x)8UK1&@Fue8vnJBe;LT>jf`wmO z!=UoZIn$YW;WAM0IZpdt`=`_`5hPLme!)Vd8FKEg$YcTSSp7w7fpzU>g}GKl?#PSvdyXGr#Y_)#pL4hal^Mb%-?Vc z=IzC!%&pmGK#F^Ez(2T{GzT(P+Vo@f01P@8QUq)SX?Nvm^5Y?#IMiXf;pL9;J~`ux zCUcsKoEz1me+du*cz|6@%R&pCS`!&yud3mtyJ&a7$FFVT2ycSd1F?o;S&{vvYP7In zBA@WMogN%uAK+_i`hs#cNl!Ca)M_!`xr(-SVuY17>0q+}ut6#5{C*ut?4NyCc`E$- zhc>Ra>iy_RI}aXTLA7^xrz`j3MoP4nNH#c2#bAne(f zH*Ej!&Ii@Ja%uIDUi<~!Uw;Ac_E7FA{$G0LJ3K&>{X|;fi)mMlCXP&;rmT)ZO_Hk) z9gw}N`(#g26R=Y)A71UXD5K~^PW>4}R)f))OPtVZ;kG+D`@Xw)I&=EBt51rS&2Pt6 zNx{>2O>br;TF4NM-(R;?-RS)di^t6dtHf1AQyhjiSP*pgDiAxgYr&|87 z){GG~x^ULBJ)Xw{ayUB?LHJ?g4nKo&P?1atg9|(qK7flVC^6yC1rWKB@!*U zC%!pZ$3ZG)?#(6WnA^1`T+dcX68vswebwl_98RE!xAmKt5uvQ5^F2ktIqA}l z1R`etu8e-~!X*3wbIWIwhvfPadbm)UAo{=$rN|acR1)5Bx{&`DX;cV(Ny@ikCR0m? z+?R&=afxh+maIB;s`S(#ULJ+@qTwiPa%|Axdc=W1w%3#qTGj&Z@P(oK?3Pje{p!Pv znVo2r#`^9GuT-f6hZF<7)++Lmo7W!?fwiGpFOL(TcA2RZ$d3=r zlR&g08A#{0_2+LS$pI~up8QbARU2hx65;3I!2Nh7bpf^va+8Z85$-`feoS2wb&BlX zv@B6QoqDnwPn#3SmI)fVoTCOcz(ZDOr^v$pZAmY7NvDcz34q=05LFU9;8d=5=p&B4 zoTw>1oZ8mkVcrQVswDsbVWGkc)#Y?PYMh#K`=NRBrTH4SdlJ$0?>w~(*^)jahh!1L zB4X2rWwG3X7`W)Ln3RLE>DCq@&^lkOWATBkV0K1uX(BBwF_}`0^2eLAnb3{?V~bsJ zZ7X{9O3mnl?6Md-ecuuXWx$IVtSZC0aFC?nKSZW~a>Tm^fU=Kfd#srq@GCJUc_~QX zd(V<3NgUCq3i&T1Ta4>}i&b-8(Z)k#W#)P+(ot)SSFle@6lppq;2uJyV>%~~U9f*Z z{6K2J!h#Uy&qkp(xiJ20)u4fiqzF4KfAHGF_ufFq!!Er7^S)_dDO~HG|89_pkQ-X& zN`pUdFQ9b=3^gHIZ}-LUaw+0!kHCq!lOF$CP|nDp``edgX16o&-ya`c_=xdP+^`F7 zUyRnFS#E8l2`I38@mfx65$X)LTOU5T-I^DflJAQOC}g*X8xK|)KWE8~O#!<61$9`T zZn-aRxi>C^DxwN3vP_L>e9MazSRH7H-L4N<&_3mpz?3zA;FlpLF{9wHTLxTrtjKK- zg7|X{Iu=Iq8Z{9R5_=GN=Lm`2fdkQ};R}PzA*uf3lJPjs=I=jpd!NWMt>A>6aS=k~ z=uxHNScOHgsgopq>$8B+AetYacSI|FANO#^tyQOD7sCH;`oYC)MQ_ z4<6gM1(&D@_1qC9r|Q<=tnn?FR2#s(w7*)?nVK&|B`Pgle0wd$6{;t>BlaA6B;b?? zmzr#-rck*mz{3D3_l6nvZkRS|^E}-=hxvBxp%MNKG>Avg(u#-L?%FH6U+Py+B%5E6 zA^aJfunVimD}53YO_?Hvi&LYy&1+W3Mjre(6pJC*p_L_HZ|(1iZ)*VjJ<0ZJYJ#+) z{K4a2`8jD?p{GORo4>bgTy}afPm^<7T#|WCbESaklkABtdZv+{-rmm%xj9uagi#Mo z;$~{QQt4b3pItnJT@DvT17$IB*y`m_*D;^z|JkPSr-6rh1itt7NT}*!iGq{HWe{~p zSSm5+0_4*;L>vB2=80bd0c8F)25;ZIKfC_jStus>cT%z@Q1c&Q^NOC)7 zHS6$Fu&~3Yb6SU_z3A$-ep(W|x6<4MXXoRYVb(a9zE4e(9i}L13Js zMcG)>VLP1_>Fa^VX@%WKV62xzG^=(uKy!UtuVD0bWkDE~QO;IDQ5FP?MM#$GHe0R( zQ^k{RG&nJu!tTfJb{5_flE^Uq`v`2Y@|*Yf`I^KtDJnS?SRc71=g z4-$R zu^&||T(*nhmAd>j#pk_`MV0|5`N@l4EImJ_FqsA6248FnX$>rZ-!Y}WeIvAuEwyfZ zUqWq^k_IZImWPDjNy9v&_8aayovb$?==(KbPhyUT!bLHVQd}Y*SQTOc7fj`U>8d$< z^Pie~U(D0CaewD?#z!s)lvX+{@g)69)=X%Wut#>MxBrsD=SpSUTHfUL%62wmRt9Rm z;&I)F9*Fh~T~sBPyow8R-mFd0k_Uwg3yfW&$P4ukRg9W>Fv&dv)>z-4(U@;pQ7KYE%h1uq z$V8muMhjY87(^1V!?k9q|QC#iETPUY|7ADN-p|O)Gwa21dO8Wq4(^ zUx&8+3Q{iDZjQ(n9<`XS(y01YnCC@9p!399b{0uala(R%K3lm<@Mf1><8s7yb2yzD zrKRC`v1!OP_&IRuj>s4eyZ6S=VY7@~X3lgJPe?JxboAvJ;*P{hp zdUDL81?quCL%-MnYPJ1+vvYYw$5_(3-}mTGvVp0vurPLHA$xYPBNNUK-BZ;M0CbUj za_nCv*cTq3VLf>WPn@y^tr?NJ-8Jo~o3~^PrLti64UD{e66q7s(CkF4 zMqGn%SaW5-lG2lMruWJ+rJNC4>+P7kEUL(frD|eUl8CizJ3OdJ(S-c|X$=Iu8&#yg z?F6IQ&FyyfgkTyTBc7vUq0r)c#>+n@lN^Y$P=&g*A4{VxI=<{Gv^{KJGkzJo;QWN$ z#&mIsIPiG=wVHPfS@RRAjHnllH6#C{%v9f+cmq9EiR58WYoncm-`6JhV|>JEV2R@l z9IbjeZh(v&>;wBlI_^@1>QCG-40oA2Y9Osv-Tk3@JxF%BtFo%8_IY%8-?tVc%QWw%nqf zvLFn{$JI1Uxo6lL`I0P=FeE33^z_p2rk8^9$NI3btQWl*kB837bcMAdV@f0P{dvF9 z>2jb)gFbwzh(|da5FJhK+nPc=o>|*e2Rpwrb)L-5{18T5jP>^SZcs<87)QaMiR5E$ zd&}+_EP4$Ik2lazIt?%`cr?c<3T`O7iYGji_2DC&18)ZRHB)&s=7fJ4DY~IM>&^0^ zSZ!eLE+wsGyHG^mxCAEC_Nb3K#k2~dR7^%*ep2*Wjt=gsuUNuql~!ZgijzU_fGe^{ zOfRHou$-%f_qr$Sd-Dms+!Tctj<9bH^V>hWIE8?6-9~H(_P&Ml5ZE-nk?ndsyS2fC z%J*66`SBntza{i(f?hvYhbkqqUDzBU_u%G(%a-o9;pv9%h9VA9Rc^|NY~qIVwvI>WG}@6RMOmADnT1eS7<<3GyX(%J61b zttT>Q44V~C@&DbD@KFBzf9*W_dBGMuWUXbeWIlnEF4F!YBsF~3iRpV#U2ThtESrWVfVf-5R3)3lC=2Qf#+ykd*VlACs{+V?rLp(LK3@i^EjH5N$zl_k2}z&_ z!G`o^#f>=#62)&%_GEp>(wF;=GDi=uq?>F0;KZwqw6^m$xigh3wpeeO;O29?%;~m; zG@QhAjMhrkEMLgf`Zm7UaQ;Dz&4Q^vpe;}fn>>j13TEfyh_#r7&(C;I5+c3gm@oj$ zfw@?$K973E;*Ud3AcuE$f)yqzJ01rrC6KfOx%aN{H69Iv7zl54`?J5N19N_C%q-UW z7rPD>8T;ew)O8adZqMen7ZvTd5EFs>94d7}MoO7TPbBmHYOA*d*H+%Dp}fLPhxB5jYEnKSnBa(?Th8&l z=wU9_cS5@LPQXY;y}sa596V|kzlrQ6)Qq=Om4#HbT9`L%cs}>yQ1alYi%W%~aaA16 znRARPrERJBuuRcGX(QXES{Q!!vq72!p}o&7L5U0t+YdDf4>xs_B^fEMSZ`JiIw9wh1v+ z_aY>wTi+cX9>zDEZnow=MQVrSg*M|ijw>Fia+a+K`=T5|j7Rg!uZGkJ@$TJF&*Yn(BatzF%N4(!tKJ?D?J1 zl9@*zk5b5Y-(l>!A8Ww&pMUu3{%;KinN;JDiX0P@(P{!0DhscD4<>w;`ylf5Q9qRY zaOL<)@ig;_T0jAnC`AB0a;=}t19p!fWHRQy04F-^95D;2-GK&%&x{K2><3-xsc0fb zHi%5g-sr^fBvPL;f`+>$eA;oY$=Igab)>MPiMJm^)Rva2dV>&pV@+juyn|438e*eU zk=Zx{F+1^Y#zDf!T7XfF57~ZP1cT871{a$wmZ(3Y$0DIAF|VpsIzP0yXF=Jk=-O!e zMMcB{Jknt-RTBHqh8f+`R?Y5$iC*=n}oeR~QG zti{Op=OtPVp*8Ef8t&)Ss8mi02HStFcc5EsLy08jhjBj(RM(Vv@T3>RrOZn6#BP5U zl1h~4lSDq2bqWwb`_+Ysux>yMk5@r15z)Oeslku4YXKW9Ny@|ymj2NF9fT%*8*XBc z^c7?akV&$UrQt?EsWArb!5Xu$UcQC{sJXs_Tu;oH2f9uer2h|JUl|qGwxt{NV8J0F z3GM`Umjn_DC_K0a2=4Cg?rwnsf_rca?iB7W!QJ6)a_;T!^KOs+Q=>-7UUTm?*OYIX z6nA^nc}^0GIekn_PLf=7+vYrhUpg_yq1gKEWaIVa-VKo2g7{5FJ4aI$JI6wEy9tMV zGC0*Q!B;bv{1ckU-DL@jN5|_p`U|uSgU4X+9?7qfL6P1tv+rj$ryC5&;cr4Ulb&;- z+Mr%-U$A?;y+sBIT(aVDWIACy`=^uoBjy0S@F4BgWcKNbXTMBPzRnF$gf#iadE@Lf zgg8+M;k}Zi+Z+}q9~zjo0>zISCQC{shro9h6mv~8(s}o4RO!A6;`h8s0RLpBbT=RNE z4Gd1^qL#Uozd}2sx+TTOmWEbfsM+Z3sPG3b%trDxWrD{iS-u^*OcacTFFq{;gS}oK z%t`-BsyI^>zv@IKbk*xh38&z=ULB(CL#IOIjlE>PQ83o|mCgr3ddcKHXRpRmm4#(X zlwok?0S|(;fuo0@I)2sfX?8ot$qV)t^`2p!O>SL*ylG1SeetGFsz(+R@5Nm=MLBC= zM-iQXu#&!rojvh7!m;Z1gpbjY4v6 zg_J~0l(fJRP$Tp^YmdL#{;Lv z31673QY0`|1jY1@?kgBA7m_tD8_F4cv<@M~VX8LwYcFf@iSE?$g>HR{WMtC4eFYt> zUk^ole1utFChFS{o_`a~b_4CBpb7XcovxMw3&AtjK>3VMrQ7L-gZ&zNyxCzW(rBuQ zg`w~#Tnr6>6#-Sgg0Kv)$!PnLPb{>qA7Eq3JImc>iBVOs>Gs!$v<;tQ5A(~PB7)*)~mWLTg)<9aQ$5J(uJ6MWnu-_X^$^nUoOx+z5Oh8~@W=Q7jM>qsixwLq1uLT`xF{mzD5Ju}Mu=+_UH zD$0cn3a$M1LR1l`A=BD*`&of}U!s`ddx=E$wPwd#5*cxr{RNK{F%QeQ_sX&Uk2-U& z4MtJVh%A_|d8sZre@_;6Y=Pm{%e;2v@=tJ*9oDn${JY{M8>I$Lb2E~ z8&v|mtt7&R;cNt>I>e))gm&fF2O6}l$at{+)EGEVNI)<<6I{ zkqxiMmou8m%>dEo&=E8f>6wu-o|$B4v4Zp+&cv7PpCyqaq%*XW>N;Y){QL&J#W zg!!%ljxFXzc5Q&;udt9NEbWgk10iv}JVbN!H~KbuybP@#zxK)^y)tJ*x?hqfTe8L^ zsMI<06OA^6{Kn-ACja)DEL1>7kD5?hI@y;YHFC5+6hz^`M8{^tCx%*5Tr73qX2t5w zMEpBQi4kBulKR;`yeniE z`fX(~(sLYA@TVY1h7-1!lOx?l;#&_86PM#4KOT&cSf_x@fmK3GTk4YhdY0( zkw)!dA{lOp;^QUp^dlqpEtTt%2iNB%Jx(e|PFDe`M_V-;T}fP@zxI@BMiMJ?{fXQy zKSjJE)9Vf^1F9-?-tpR~s7Evz;*8wn=IYb^Y$OTLdVD@}?_pbow*CDMf!aj18Z(%R z+t{&614&+VsrJHH>Sq_e-o8*~sOjmss#)d7OQye&?Mwaz($>Sn&~{34sH4nOpXgev zBB23>Bkc#r-wQCEh+N-vpRmd4G|DU1JtsrLtA$L03km{zaV&HXMPJ`13&>SFfheNT zW@|UUDOD^wm0H`IX|ZJzx*<;d&?Iod&oVDmrSHDeSxm{RHhA;Pokv`!{Myo{vS4Ts zVXA>T%$2}EGNWzKl51u2aERc121C=O)k}U;Qr5jK!VSN@Yg2sUr95t;h#Xw#=o~l+ zZsy@f)!MH<0hNm1rh}OR-VPwgLYH3slli<^%A0HdwfnBUpYB394@HXEurGSE7UTQ^ zQk&^16p0={o)q7iSy4rL-&Ht`D`@G~@nJrAoqZDAKrZ$BfF-876GBN46EyQz#ZQ-A zL5qdCP)32wk3^im=gqDNWC^^67c)DHEtRvFOJY1!TTwN6(?xs(6xC$~NPi;{!u5#o zQG_Q)b)D$txGQ?YB2=4JEMtrL<4!(~jb@B_9RD;)ikcxU-(WPl+wdoekr1N4CFl&q z_;qU^$lS||gClh4e=nAVx<(qj-XPW#Q(@EPB$e8Lj;K35_2QHY;Cv z(#lDUF!Im}$f4`u1jOZ-YoKIg+nceO{or~$#05Pb*s`Ypz|pBZswJW)?8td{{oDmA zkxj4kdICcsoI2w!Uq08>Z3VAd#j^EqEaREe0Xb}ZV|qra(}mp4_%U(GLY;tsJGP!& z79Uttz03XCYmasfhoQxfds9P6EUf+mTNDK@V{zCnbUsPe<6$s|G0rxt9h87@&h5*9 z+!g{X#ovx}pOO}3;IOw89@1Fbs(P|1nRtB%j&$0B|GRW(12^AhDk>@|h|^r{`-huT z9t`7YuCis}><`gFp~~C$mR@p>HiWRPPlT;cZ7|_!KV7ee*J~(Zlf)&#n~qrIXJ^0k zz5sO-hhi&N+xHX3P^)wTJiU`dwj7J4jVm?`hN#(vylw@5FEW~pADt!mMSmC_P)lks zsv$G-ZSk=De9N=OG76;EDoyR(9%{e!JB(%WZ%GIv2JNnydsd5>mfLGmF|bmt5{Z2* zm?=^!OCetIgX zPfd}9z;J1#9o?MWF}o~vAMurLc9H@L+YJ&%) z%wI{4<~u5zEjXh^IO4C&Cw~R$jKWJM4!RqX(jM|7Vi<*_Ouo-~YLlt3evn6deT>h0 zwcp;EZ0@~AstVs%Fgi?Zp^6T3)OXCi5ai)RJYQGVncKIHNthnIA}lL1hQ?%X4ig$} zg8vQsz5vDAFglG{r2s#h?4zZB-9|X~AsG*E#trGG^BpZ@DqH4p)`jwGzmo;sr9c^^ zzT6l4^PafAh%a@SU12Sjo5K^ee@6rpw}g%|XGErDh9{HYho)^aI*cBC?vZ8y<;+w# zE^AeLo*>ZCOc}_iJUlm|$l!(dIGA1duI|`$cG!4*50zo@T)mPZ#5ZXNjHD0p`%8^i zCm&ifPVmlJ-^i=Z;C&lO_q_kvkd5gikJ&3H-RsgcgnwVT`Rd@T8ou|N8bJsI0AP?m z>O6FZM=C2eDAIDKm2`N+-LXW4dzgpS*eL*I`tT#{eSA`Q@tWoAxwf9;U=NsTDpPSY5a(XUaJKo73lIM@^B@dPUufYp3EnQYS5n`;t_!7w9{vom>>rD z_iek-&#FxhIwxyfm{^~>yfWVwf(q4E(_AS|q7rW~fQnG7C?3RvvBIxh_zMqJZx3uq zqFn@;7#W2vNeDhTfQv==^p4LuW)G&1*idHz`gN`vnMR)5OJ5o7 zoc~)`b(Rfp8XD@h%i<>|7Vbrnyl|}D6po}aZA>2&ztIo-ucab{^)>?iL@lpx@zz+R zx_uD#oK!)QjHqi)!)Qe8ND2_QA6Oul3Wv1>dc!#%tc3jqG%h!Hu())vE@vPDX?EJ* zj;KPg*Q-fm)?bL*#L+lM&38l&hYKQPc25ey(Q40xms03oM7~$zS4CAWx`BJnGKHh~{qG)p*LUP3eh5tGyI17}=K9ml z7ZP8}$4wbZwSJ;ev!S9j*g3s5r>AJt1xp5u&7(@=cbcQ~8H{`EUsKm-CDNmk5)~sI zvq2GQljkqi48s+*u1_Sb5r@1~R;XuJSNDEN_ZeOo5rUVN28qh^$b5C-1kvg)6Ul5& z33i}h8VxceoF((XRZF|h$)Y-|{c9eL1{)ku@Ll;)o8V#W;=Kis?cI!}I)Uo`*SX=; zN9!0`$HK=T!_lg6&0DWU$Tu!Q?Y;BPwB+|XGF2<-71~i=x{q+dxLh|cP{i5CMA0dA zRu*=_UGB2=ZJrk4^rNJLf$TqlCXiO>Z2mo7ksT53Fzg_eiR`f@kmn6m7%^AI?;d$wmmN-s}e(0gGrP>*vIJ5U)-px3xT^~ombWu#z9kY;>h9gMaI zTe^~_bGci)+eFzSF1#~l4|g}Yi`mm1P)9BwUsn1c`$-Ns5x!#2sc9n$nI}5xb!1c&$&&R-yDuNbC~RSd zu2S$H#6=;v&Gg*FVyPGbpYfK+vYF#iCLTvOJDWlvU*{;J*~xZ4DIc2};MTvqDMJ%i zx`*B>5~n-~OK%iczD@-xm1%YImt$1845g$6PSyZ9tE4(vr%oe>&G${W2#Dx!dIOS| zV{j@-$judi>5`Fx?zdxgt(0uEdbW$alYV%xJV=VUA3Rn&h?yfYl_63$G0ASNlt2%x zoPN)fE_AaOZvM4(L$*slN;8Gw6~!-coMb0soM!v*mq*e$aLv`?KkF^fI!r^$Ce6Q& zDJvojqK=vcs65y!+zO)-a-oj6xI7GWGTuJzUCibNio%FjXosxHQ%p-bz*xSaXiCE1 zRxM6Q`b&;(My#f-C!Bt%apv?0!rSq3j?j@zj5yny*ovWP0V z6<(E(Fy==Z2_LqEU!=N@qu=OdKK9h39rpV24Srd(AQoI;*d~9{uJC8lKBL~l%55qe zMB-R})W}#)D|B{?bH?}=f>uCz;qR)=a-CHv{I%w&=!`z*6RY7+ z3V)ID_Pl{aQymteyB%Z4n~#TmVu;i~7=(LqD;el7L&-!SnQYa{XMd5i6E(+r@A5}5 z5*5d7I2CKOAQ87dQFU~Nz2+%Kb-A+Wt6$EG+!Yw^L_zICgPWu&L$dc!g0CYDOaS4PZDg|@E?K-xW2eB*{J>AX!!q2&#K{b`?C zTWPEc#K=V1ngSsp8~wBgpHdoIOWcVmC9{PAGK8r#H8J3vA}$ zajL1>zv8@h(z!(&U8=6BGBy7oepl{fc8lhe>%R_(y05@bNv)Ryn78(oL{CvpI7)1FgU(6DyPTWMnEWDlJiFz_L^!nX^jgG&WC5 zgtO;8oX+ImK7i5(Weyja+mRIt9Z&KnJb)n=DRk zDeG&fwGdAEnDJCOvXm$bR`OqtL&;nmDk+?n zq*eIziB-xupwI|~JjuF3o7)d$plFDfF+OeKLX&gX`H{7ph1Qa#N#1^3k{mjyA(5$% z@8-K?SEK?c@3e5^I|jC7#RqGO5!1+x&D_MW0bOUOJFn|SA>H8tY3hp8`ulkuk~+L(K6M#wLs;>C??ptf@;7snadMdVo7C4X7!#jpNaOx}s zBMP3?WM5pt*D#XZWc<|Fs^eWlu+@z!wY)En3@k725AU+OEAXT{PSaTT;5oy|jE1%c zudm+=Sf*m(g9dy3fvq9=61A6+h!tR2-XHZ$U93YL(U?KKuyfXzfd-NT22eRGAX!|1 zNP-NfHwf!vY9IwEW(7j6**w+7-Xx2AbXUT79={L+5w(Q|6u!6+R_X(Z4{F$J4v7|G zUQwGQsyo(P$?X%PoK!&)97=YM0Z_D(uq1*w5{cIf*yqEipi=@yO8%4~WwlbOSfZv$ z^~gTUf8hCr*Y5fg;-w{hVN5hWlGCvGM{U@yT2EVfgf{v57b#GJ~b&;TD9};jv6%4hRM(o&Uum+%D3qGyo>}B zqr5?L#?E3+wUbt|`V7bGk^~8+Ia^NQXdUqW*pq8>Lk1}2wZ|y!J`$%or-Slgg?l!oGZJf??OmMVOt$E3gspADN$6PZBl3g18Va0pDz zu~Ql4rH7a@)2Dpl=-QC;C6_6~y9hHe`L)n|)yqDRi{+~V!S}zh-gz}`#uh;loY}@c za4x(hwM+?xFvY%9mlmJ1OWxc&tS&{`W#PbA2T3nyR^OD^O0$0J*Y{BFW_=Lst}Z<@Q>_BsL=LF8f~ckLX)6hNd)7&Wab(b6 zqmcoONC&)aZ$x*K8#fSw``|lmcqd5KXyf&Oucl_)$Tn9X2y4s$2NZ`Y7fcxf0PNy&U`dQ$;!O{ z_M0#%|Lh@$GJ56&I^aXuxY-E9wsEw6mA9NVCZ8?;^2{#F7`uN0Hke=?n8E}lmKy3^mnqoydeTg}?t)$5Wd=Ww4ciyfA{Dkw?pb`)>o0IL-Tf10&N{;e zW>Cf&M5R)#2KrX1hcWnqfWEF!>RhwJ1^MdUR7m;~*WfD+g|8HIU!cDd3LkWRFvh_1 z8gHV4O-!11GA)iO`cjSg`%x5=YBR(9L57WLRh=XK+f{Rfayebh2tLSz^%D9KJOL~b z#=LnClAM-;B&O2`tc3c@3Dak|oat|r;!xsE#bXlXlXQHE%KGYVpdV`=?P3sxi}&Q7 zCSOFWcBg$X7u>&Qfjag*4#_u(iYQ2KSKQa0azWc&=$so>-d5oPYFNw8Td>(hvP&X< zV~XKGdAntod^h6$Zn>4!ao8-`g7+mADdsG^61_I5F*tnoEtiGL^nHnX9hg|bbD&p_ z+~6*PF#5m>U{eiYK8&R%=OSlrs*pmw;;S21$frZRo4vBuT4F;LPc#9vKRt|YKQoK%Ltdy4)LdW={uu6?fXBp18*2@WwCr8} zj6U$$U?3GeA$H)@zgfz2U>=Os>~L&`vhzJf4Guz3^m^!E8fzJvB z+sYXVT@m%LZ(cB^e(j^nlcU?OgAwozpYXB9OtYg75@+i~4dVjn7^F7GsA|!Sk{2jZE_R2s=i! zsNfBErap7JOX_F@vTRb;6x_jG$A%P!!rjX;sgw&2ovfD(#v~u%=nXNt=cLA71k7eW zF4k{^By6LviJ*5nHj_msO8>%Cq;>TIKV#H6BHPM~mP*ytV+NRo<{=1*BK34e!sA2$~Jf+SAOBFqIUAXPL*CmMMF%TI7xBAGsyPYY(*nL;(ClbrI4@ef)9|O1v-MVB1qs`|Xd;1iq(;;j=!lKxtBwcHi zlA9w!jTgJ~QJk1DN89yD{RZUj*0OCJPOq@~#}uY-b~h5u?Gu|ZqgBdhf| z#U3UEoJ_}{Uc(L!)v6?xhoBLFp+!9UE%jS~v8@mDQl8y-PReAWzyg7`vgP)N-R@~U zP(?JlTaJR~Ht&LIwY~+Gh7uqPz-T8tBz;2w{cS1yF&Ca+u;hgAa=GnxvYHfrmd8Ih zpKYfKCJV+pn9r-N8-Pg<4uu>g^M^Gz615=%ULFtRYq~Mt;OSuwVsp%r$75Ix=*D#X zsa{IEJv~_6xn|8LKE;N=t?T`k$-(hpRRSbS(y0%eVEny`(M)+u9ILy`%y>eHy8dd1 zSPVM|TLFi+;hy#2RWg^SH;}3=QERqDW~}8gTT4@hB+3n}qc^xCCtq)a_x|WF3}XC) z*IH$^;JM^WV$f{O?u*!0T?p0h@FNC?*V|vJ+1+A(GFA9) zbTL;GeL2&)=Y3PT-&5To_2Cbx|Me0?4?DNt6Gy9{Kc3rn+FGjJzh7@#@)5{*OP&Op zIz`>+?aUR8uu=^_%$m-sdF9cpf2G_cSzwy4FYN;1T5@qzx?hNP~Bxvn#zQfq? z#Ry3iyeuY~q)^&dvDqE(>a{O__Y(F~mSiGhr7;v=wM8$v`e?MBb?lAkT-myRsKy!A z0~x6>*4PNi&FBB3OBi^k0pIf25J<0H`->uEE%VX$ zk-$cO%=(Tjq0#8jMU*!B{T3L>wgDs++Z7NZOS8Dkiu%O%lKvM5|6%9|*g}PTmiC~o zsoY=4IDM6Q1DlnFswY6vjCe^3hrz+`8YT73+7`NZ(+01-gQ&6|Hi$NL)` z==qJ;Mp(ZMo2k~D#Jf%BN1Dz31WgrdG2}@mgc${X_P{!(K7MpP0yOeAxJp{N#2GAT zYQ{TkVUDn?CmKnrKrIt(u3oDO7Br^rq<|-D5H4MC*54jZzwEr$U2t-KxR}m^>`JM_ zjnBR6sx6r}*eJ)s=8}GQXM-k+_RO?lXQGT)&yN)FV*hq!g;DpHTSxeLmXv_7J=LZV z9TU)vCTgx`PsLw}$7U-|cNrtzHxM1P-10za820*9v(>|Ui4&C!a|ymW>T)YQmDQ^4 zMer-8mZrPIMplDU{p8C5_9C{)&ybr`qUG}__>*->ZK&M681!AxrJTqIk{lFz^%jj3 zPIE?Wk4OI|gIn#sdlP))<z3w09>5M0?*=&bA88-!QRdPOE#M*53CrfXcQpnTF@3bW+_5<*xPRxMHgxjX{`4BBao5o5AqN@+CufRmNXt) zHHU)*Qa-7dp(c|>pi2F|&WnmDo+nrj)yfTzeNR?}XQ<+p^2)h?71Lq+-AXSiUN6&I zPRon-gc*21w>&GDYYCu}NaZ-d09tKs(yf4KKEEl=+!)DRuGV~PQ*PAZL-Kp?oefNy z9-g)Z0iytN@01twblQF7A@ttCR`xNyVKN9)rCQ{v;NzP;BJBaDR;fhu`C3(uK(_`Q z&H!OD zO-c0HEs7RP4U_{)#Zf?Fp(M~VrGxDKK9096mXm<8#qMHJeodKW>w)nkl?0}cR>6L1 zIteB|Y%mUWbXm)7f5S-RJC1RaM=JW$c0nwpm)g_*9>mf#OUD5UfJz2fkm7*Ge!&BMdL)e@bec9V&KAv%})9?zu(1{jbCN{A}8` zVLJmNqxK+Ezm^0%LGzafn|o!A1MQuG4+o!3znCpneo0}s*taCY!3+|ICFFC!b?MB^Ws50bU+~$;P*MbJf;WQr9P+Rm*i+!rlqYX>UPWVy4&~Yrh~S z88U)amAwRMI*#f|zX8_EC)jg9J349Cetg}OR?hh^ri1@N;xMS9lse2+svUb3(nLHl zsY>LHL>&c%XEGd4qZ~+JjORR<`}FfZRu|rfZnPHs7OLcF3thERzOkPg%qkr|1yr`T z>K0KZhbbqm=`AU*^KX5>0Z*r)a;5LGH`Wx#R=QlN^FQg zZ8C8kw<0=|<|BL%|3WM)Nd@V@WhwA3s0p@qMRbBjE>FKO#%Ax*d{3w5h4Q@PA7|uz z?$N$mYPMXrncaA5!o#UvYsC0MT{pAs1#Gk^K;5Dg_1weVWjr#DXJrJPb%$sS?Z<9$ z8^3pmu*0x#(rDzXlZ>qNt}>dZq|8rg#VYlOQCab~VBu*x?s233L548NIYKpw!>W&e z42cJS(Uza7)d$=2*3u_7gr9GX8+p!UFkI1KyaC&*zYPZNmx9#?M;^Bs@!1yl)QKN* z}#OmCSJKJO&Yh?QkP(GEyz+W+8L zbs|xd<#Rs8?MuA^J74cauhR!$15JNdF`f?YTV26OY!DFLmRY zRt+P^%!XHEH>Da%sjEKSSF!E5AlVvDAy==ph+bO0uukuC0{R}QUVeNTBAiq8`d{q& zw*dfnlP1$8u^ODdV4p~DWI%&!gK*3)vKARFjAEmOLD>^Agt(&GY3Wi| zw0Hb&Gxk{BSV2~>wIpI0TuaA^j^=A+N<%5ECg(<~6?*lpH&VhRWGF^adJ8iwF(d4{ zTPgAsl8lQ_(_q0uS&;yYZP)N38I^G8F9nspDs6a7q&isk6Wk_+^#K!)|8{4*gk|9+ z5@<*P&4&+ENU=PLPZY=v;sjl7?ksx_a0#?V6Y@DC8raf@3jchyb79Uz`y}Ul!gi3b zXHQHSlDW`eF9`&dNwqyr{L>n(NEe9V2t-L}U{SZ2qR@8PRIZ$ww}zf<4(2#io2;_W ze?wELhJ1YbDp$2gEYi3u4TpGx@z@~tLwf>`W8Zs}UbqbIM6*6jYj-}d_-&)zR_;@) zISI<120VLSf84J>FPOZr1IndwfTWgau-nZGGe+Eo&=%NBm8jK5u|pEX5&?HBZIBrj zOG!ug$wR(ag#`#~WLn&Pe{G)8p;_e;E|vNy=_l}sHNxw=@Q9^Luam<2!#mQC`uBdw z5mln7w)sgd%D|PS4oIm9K9T+K@`${#?r^#(1}PM(=SQ@9R4>tLsHoMLFk~v-vF8x! z<@Rxb`NdKc`n>-Kl>n*jIHOw`(rMg`l8N-O+!bH>%;`1f&Mi{Atkr_B;K`!AO6v19 z>TN?2$u(jTH`cmB_jr@7;mL}WF* zdF9RgYSdIP`i78oLgVaUz9#-Hm!$+3)Y)oyT01duD0UO*dlOrvTq+&{*dxkNrKhKc z2d2R@DMU=92rsQOf2k_z$s(oLtrE3f)7eT%z>cLftu>3GS7mj+zpmH~+^KCkmzOtQ zDlWX>!AyG)yR>%)o+FM-^n=Oj^|Qr^M287gWDq=`mc9Y%#c1U>1a7KL3{>mQqK#HP zoy1e>M)+!NQ{%UW1Y-kU{szVO$LAV^LQbK66*f8}=zX^)RCNQ(jjqbEI zI85&s9halmC?$G-1q&sod_dT=g+7HK9sq?x!d7H)WdtU&eyrS$AAz_blkm%sm2`4# zu`y5W7IB+R7tJ>u#*DUG47l!dy88vX<2RueQ<0r3o2=KSnGf3q_spx*H>X9G_vU67 z{ghh+9&h^M4(v@c5Wjp1e@$0qI1)dSQg>O$kMp-a2%>?V^ZxPZwA^4{q{s;q%4=`Z z7m?1F=QNfx-T+ol=K2;P#Vm9V;5mpe=C{TtF?p3HlXMCN@t?7&L1dJY3FWXh>AZGP z_mIB)XCR?_D3z>p)dmeIi1RC_%iwDQcB8Cn^LZ_YgE`ulQki%u-4O()4^(%j15Cz; z!KResNJilL2K%?YzyQ#VP^O0dtB-#TBw4Q)ud7!&1@F&Pd<1}_#zNig77jmgxRZB( ztrb-gFLH3uWURxNqdcD%;a(J@1LT*|^qRFDDAGGi=f;5nU^SKY;D(wX1G}-GDGG0* z3qVE0drno|GA;ZVJM)rLWm=@6+aock`7*ub?GIwUh(Zs{5|48=Ul=oZXemSRqRmvb z;urZiRR_{oU;;gwUDf8Hp3;xa+K88X44%X@Ux$`!txK8_{f43LCJe}nmMjXLq#_T+-5kDyhJX}m7NcCEF>+U~PmZ>BtNFK@fmq|aRLkG17j*KMZw1@IU1 zrI)X^Ih{FdKi%=7{mX6`hvhJxjDz0-%>xBdk`FHS#VdY?w$quO{rXTCgU;^oyuDZ2 z0d|}-g0R_SS>|Vn@4uyR7OBXMH^p=VphP;*sOj@m@#7-h;E<(>Ccvv`u&K3XoJ0Kf z@uiwi)Ih`h>c6PZXSyTbzW+9y5f8|LWMQ%DUaOz);9yzJu90S|t$pqGF5~a{cd1>i zJ{Y$UUpDKrK9~=s^1Uj=#0Nmfjy5x*XZw+42R2UvzfLJdz%gS!J~nx{NH2*5$bFPM zIJ4)EHpWngN-|MKo6T0H{O(uX+#_1l;yNU3p>|NbH0SUoYN<>p5byNQZS zt@$$P#aoW(biSv!s&B&*BZok}?1hZMWtuZEK71RSNlODq@n%Xb(DUy@0yu()5O*8* z5Vxlm_ZT5hk2}Vi-Bj&wLvi>JhHbhpMRWOVUc#dIqi>pUJX7>J_UsORbAXb`@*b=n z1sxPWg}pI=#jD!1^|5hyg-BK_{04d@8fn=Q57=L7LxMa+LkZ^dxkboYX#%dF)C3+h zH@Gu(;mLA@3o!(va=JnW5{6PKKS!iV2bSyiVHYYjr{sbYl~@f{+J&j#5l3R4p0m)T zetbq}kXnQ@H0q1P_m|G@3H40>QqENTK?g*dtG?|(cCQZ}unmfbe-tSf>La{NsWE%6 zK9qQ{S>zEbpC=&^+kKHI^VB}Y%jS_zNysbYj8g4#@#9qK(QWFy0*6Y1SYcl8>QEr3 z^Dw4Xx#?SvAt;SIx+e3za;e1p<%o&+{zCo053Dtd#UEej`hiM?T)oMZsr?(&M3pj} z?#P5#h(dQpb0)kvO42=0(=xS4mGrmGBpif9!GeK2lVWdE2WMXNwB{drXvp#s@a-_) zE78|mo%pz^mHe3ZP^!)haa?v+uC-joUyTSyBVUmg=KG~p=MY}al5W^2SvYvlUIjb>JU3TS(-2~D<~bfs(@Dv0DIEN<@6VNO4kqTEd&Dok6Tb5x^kstR2gpWh) zQJw3oNO{~gKse$ei>JPU80kJZ4UhG<+a)W0=&$G$WwoWK7rvJ$&kL0qj99#_W-7v| zhTBVm)Qagq0n1+hETP2a@!t8ycwC>y>Dn^?%hiF<3i}~<47au3ED$VOr!ZB?r)C37 z9xPt$e=bMV`&V4tFF`o9*1TBrOQE7F0LYMyfnRq2gy8GEIdG-5H4G+vU(0Se%U@mD z>4AwP=bc(-0cyUzD0T|_s#W^&z_=F4GOY=tmt#2X-5Az$6CjbeP_9?e<7?a6TCQ0Q-!{yJi zi1+vR{EQxFTMWh%)pY9slD z0AOjE0X$&{#3E5&0&YXY+q++6V7dxQ}l>nPXu6e_D&tfft4f13j*NiV8 z*Y2|6HosTqQEDyx96tyh_FftWgu_OY!4RRt&kPP8BM`87>G;Q<0>7fF;(8IqRIqa5iKH1i&o=+Jm46zYG{5>7k#}L zsI!@rFgCn>^?SNi2>35*rH&}hYc_{8!EME{YelyisE=NDY1@^W=JP{9sZy&Cf;`2hck|R)= z2Hc^!F$@A88!fZnKSM7488lhb4W>({M{ma;p6$K0qlfWo5P4tx8OJ;sBx*C;eWr#M z=<}H%?f}KQ(#2snp3NzAuWv)=wwSMtFjp&a2_M8MY}{Nw-P{;SPtCLbju^x8(Z%>! z)poYhzG$LU^&|ShJS8+=ItGOpC#(GCy;PsRdk-it%t`z$( zVUA!E%d~2IaGg(^NWX)(X8=^A(aRxZv6ua9qT(a9LRGW0zK}))1`L7j;lbCQumk}6 zSH+P0fbH@JWOdLE!Etfck$JXTeqwXGubTbrmk}M1%7(o$@J)Th@v(j9Y_rLpF8@IN zib>OEZty)sv-b|;P<4o(X9#Jnw6 zO-n|n-CA>}$y|SJbP|MchU;{L6^wdM$z`!z3*N%}n=P2JcE;N8^CE=QUJox&Ue&u9|bg6P_~UcjJehgJ&w1LXYwenThh)$tw+^`Fk}KNj>V1D3j0i#s6w z_uBvX`VWm>%D|mz1*`n?;{P)k0t4^H04z0~V}|j6(A)$>^p7Qe{Vl`(bD8J0R!o7V zTJQ~A|M#!|(75u&>xI8E8U8;B_}fJJ_h;Iuz*03d^MA~Ne}DM@J*hH40XAV*{2$wZ zbgc#~RVFq|^ncLo1Vo=#Id9AC82|MWmn@XI--c&hpFqySalBt3`g{Ds`ydQ4<d#&wIIkPa=fwac*CdLQSW4{YtB>9lcibd^-%3W7HO`*N?JVZl93L-VTFbzJ z-%Q0?y0{r+rnAWi^0;rMxi46gXzkBb>MThCk-4dS7ojGD^aV@o@(R(8{B`eWJll&n2=*@ zqF#jOeHF$;Ks-O7yx5y^PQeRc8 zXG)F4kv7w-^J`;MdnieEE(XL>f$5-BGI7pluO!wX48HH|g3WzM%5p(pAMhCOYuO95 zm0jGRlApp;S1cT*{)3Udt;mFlaaT7)qh3Dqns*!OMqj)FP!f%IjW-xm^W#f&)ziE9 zT?J}TJ;nep0<}+dwlMv@s&B)i4^C;(W?!uORIwV_oLh80yTiuS=}~YetCOr}UnWzT zf>j5Uch$h9mcFe2wx;MFfx1+qp6==KF7wM?J4YFDA`|u7>4z=e+2v~hX$7XqxaO1{ z#vo?29Ng3tYd3$8&v%jk*u7};ztCKccdS8&U3;T<^fx;?;a(f`{~~!cmCp|5FYQ0eS~L* zYHMs;fpMn*jMb%AcB*T=W8ZD|XRPls@sk2Ye?PX4r2C+_x9l;6r`E)9c$!6dLzo{I zb0{~aO4KhkJymL}-Dm7|dLq&mrpA+V&U}P?)p$cVf9HVz&(qkE_%q{af2QcX^;a;6Z1!?n>!y+wFLs8; z^7FqgPexxG&6j?MTAXf=?6D6c{fDyfYX)w!Ti!V849lArplI$GV^4vKi5_Ru!nRY`$^Bqc$uS2@Ec0I z7MEg!vO z3?av*7K|Q9-98S%f9O_k=I3lI$o>AiKc^(M?U)pf!gxIQva7*;@?-V)-Qy%|3MrkD zDPSt+e&->gbfz`Hw)?wD^2q|&rNeM~_up&<)eAuw*__e*`%4}*0NgPZm}PG)~>PxxZLVH}% z)q!}0b@7=-{&3q)21Db3Z7ko+ zjP3L&h}#w$=@em*unvdRzje!Y$P}=i9^!g1KZEg#ha%3%_ZB$h%Ph;c-52ZP_ZzDy0XCs|20r7*V05_C?T3B2(@L|`sY@ig|(Yq-R z7Kb*O3sxV?Zra3@^>XNU?=+BY7cP#mz8j#vGw$OP*i2Lx8Lcj>_1bS|vY zVK-MC#Hxm>C2Tt{c-U$a>-V?XY-qqlj<9xl0v3fv+?Cww4&SvY##&sgAY9nabbk0% zub3i@i0Vg7n3e&aj!YTGi6iHB(l|2x^e(oRV|(9o7q#X2-TvgwX~4U+86S>1t~44` z#!DgrXq|Nz_c`ZqWunviblVaRQ?EwQ@jmi5-E~M{FD$5|!!Yu5%lY!NUGm?syL8Nc z9tcNmjq_`sd2|At#3|p5q)i6HntIOzsxUWu#gHC-mnh`WH(a@MA<+-=-nZaR;%W>G zDz95Q9;!3dBdM3)lD{=7Tu*%-o{(-QS-eNq{Qb!L`HSbW%9D!>k4L2TE_@&yUcxk~ zQz=AHSGHYo674p+xYC(W%8?YvZakw6K`@Y?gJE$w1Ru~OR=0fy)gOl zhE9XX-K^UF+We^TI`r7@?*s) znpe}>jY*T;qTD?WmezZIo4CPI7f7jZ%2NgEEZih`FSMVE9Qg_-vS>u}mcyt32XXoH z8+v-uRy2it^egL$An(*A?`zs;T}&2@DkEN-dUQKpnhxOUXqY2@#`M?<7_yS6$B^>_I(d}cY`|jc$Cgt(NLBWs7zFTjJR9Eh0*&v< zq^*Sosf95MSX#>s4{OQ`?X3w18moRF^`r8NJe#!U$)sx!Q~Re6yV9{K)x!h<%QZfb z|F$@GgY&rLtt0tfX6pU{o5Ejn2ng4S+4|cVEmL3K@P*HI#Uflxp(Z?8d=%%9qKhLWHTZUY^*p&jCRqW{j zSG7D5QS#ro&7~Y*f3Di1-}>tngieNj-fjLMO5KD5wf#P-EngZZr`;b#4@GDJ5av>U zHT(rCk}Hn)Rm|58vHS`!`h&`{Jd1T36HQxD#Qd82`gGtB`f2G&X~%o7EJGL(o-`_K zEMwDuW!%p%-L{kzgzhMCxH5JN_|WhVZ(>R=e5w8R zc@Nb|&BMJs>!driGSLuyK{eppzBf*!A04g|-k)yfw|P0;Pf(>#AC&m!kwr!;I+UCS zJ504`>y_LxulxQqP(pF}>B3(nx6ytNtMD^uU&|77Z~S>xEJEeF{s+J?@|*)Dgt&-o z`l+a{kLgwRRC`QC$=Kwx(!}8n z#_!48t&`70r{sZG<|{AvwV;2_TpM%-wPOmVzXlM{*4a~gRc=J4H&GY`Jn31x8V|sw z6Ber%+q+V_(}J9+)wHmC1$rcA9s3Utw_kn(ycw#54Zpqa^~ec;jiUf|8!1eSEz*sf ze5Rn1_g7GZO@|>puJKvSk*J?e(|!gzp+}pG)hZ%|)%!wHy3K1ZLusRS*of2bh$}$3 z!2s7pA-x+$8GoDp5A?D$wR$nt6J7LI9gLTKw3t@}KdsPQs<>!nk`fnr#7$LJf zWmQ>y7W!;@(!6n`vxnromugR0FMuArAHkv(C{y?@!pc;_JKesp*X*4-22H>Z{7arkWVRd$+sO6a@4Glepe( zHo9J8VE+nurLlsW!8x*J;&-$eS>ElsgdD1z?Mx^_)3Kj)?@G5|9|he6Y~PZz9Q9p* zMbObHM@WaJl#(C(H1Pg(nqIf~0PqqSE?=hxlph2*T@L`*BgE%Zzz)x;Kakk|sMP`Z zX^%{B0*q8v_x2dM-K8Jw&cEd*ruJ2luw>I z^ci>}@N%|IR6rlDFW%@x`~4d0&J+N2dZt}h`p?z)^`FTCV5^HV{-XbXJ7$Nx0EaMB zr~Bxi*xCQS`kU|mWFI5?-{SHs5qee+fET&~cA>wG{8vpbs5}XA`o;VC9UQoB4miZ` zVk&R^!&onrYPWs4782#{B2+S-UI-5CeQxf>E9PESQn@rd~9)giS5rz z@TZ|4(tX78pC|t9m&!XJv~bAF-TOCk)vs5M;d1RSMD6!ZFAVHK@A7gAz>fZJD-vwa z_FLb82XXjEYs_8(@F#}Dytg9e0X&>Ny1{DfS1;t_Nne0$22f-nd)WV6H#`C0)H8U5 zuNVrbe8)U5_<4^q+t*a_r@Wepy1+MN1rBzfj~}aX>$pVNul{H1_irnB2t{|dlRmH9 zsx=CDdlV*UA0lH7mi^J`P#kAtbV^8j+#<5)7qkjC{Dcym!ZP0G#k?HQH2KSBO z$05hR&lbP^#qPggPj|NXD-b;R5_U`<*syF>aZSS7+$SxbB>v_udG0Tf%lvrwb+NwH z#reSE;J>f-Gxq(!ww2%#!F`k9hVTQ5ap32!-A9)N)3FCCR(ON}ssDdlDK{+smwXDL-O02yXT4rBZW)$+k`FYfhkmHzkrxHpjty+9i}?dFiby`%pq zgGr&isNla(yk9qDvj^G)=UJ-$w+#h!wi>q=Gu5cz{D(G&ssMGTqF;VtPiQ`b^RoeX zs9)LY7~ok)T;TLizE_X~fLdH=KM^N`J<4Sy;S%nMo9IK>c!3^&!ga>=vC7QP@*={~ zVx>_ZT&>o2jrUuWJ$$^2fGOjly$Flc;d18`cYhWst|+7AY6slPg@X{V=}wV}4Qbgl0+?``GGpzncoQQq=u`haY6F z>s-h(hwum-s})ga9=8;J{`0-6Y@h07rMW`90Unl5dl?W|u&Kh$an1laZ4%DYXYv<< zIRx)EzTCa)9UDt|7vhYmSGrnc)Db{lHq-;^ffJ_@@Ie)pGj8say${i9 zFbS$({!zUmTQ7}diIVE39S^&9pw?ur7r9$`1UYp}9^t~*7Tsj6T@O%EodHhBf}(Pd z^p9E?=a36fp+ujlQGdWIzTda|e3?e!J;Ag*Mz1g4rJp*Wy;~R&n#L((H)nHw-7>W? z(%tNvQ5Csb04<6d1q4+_`4yTRRUtEBaHD=MMc)fR9>QC`4#T!)t%THg=8~vrzA(+T zEVc0!*O9RM>j34)b?$j=Ca&9}D^>&JdnpoUUG!liW_>2diI`5EWwg2e2&3ia-Xsbj zi3tWJV{HQ*l_3>l`x3eU$17FI$HFy{m~Yh{Bb09~yMM`#nZ2!K%;&tF91AL1zWOAL z##uRjyv~0)8HOp$W#;u-Ovv>Uiov#lG-_NS9ktv7BXbA6E@U;8!RO3^2=@(NarDz} z9FldzM7#MDH*e3nKzP=69yZhfMEatBiaK-gD8i8^$a$C-z0^682%kl-Hu%L&S+Ivs94p1Bk;=)@xy-#sMr3 z!_07zLE!~ItS1s=(GY0v50m|X_5SQ0Vx{ahQ7-}|F30#WpEjMlSi4i<=SLP@@nyNT z;;~BnJkvQscoUQQ!ey)m1{r>Dn+ueohJ?jlo~kl?pf1Jj<%Es}C{SOQsdY(HGlzx#@K4?KuXz25P64`Y(^29RnXe2Xe?g*MdOHDak zJ!lqGH0-_Pk8$q5G38^q<8G6q+05j=ux_*S_(zWp9E=EfHi!0f=+`9hvrO2oW$bjx zJ^W!EM4*GhU)rM+Q1mP_hXH4nBX#1ZO}TAxL2cc-@7+UKfkjE7{*$anAL;s#E>3ms zGm9RKmjmGabJ(oqGx!v4ZJ=I!rQ3X8Zif!z*cJzmRBxuX77zo8vi4hUW1R&u7u#Q- zlbxwQOt&LH@@5B!TxYd%f&v3}Mo;oDBtYCgvO(sW(PK__bSLj-tHq?Wh^mHcA8$Sx zfT!TZ{7~Bj%7ra2YDm4(SY22r{^QyELTwqU6ux>nw`8fw)xh#-cT%UEK&iV;hY>Nm z$goVqnZOWuM4#U2$>DodPH2rh%3{NH*Zi~tSM?h0?|QfuTZ$YL&^eFW1fr8eMR1_# z!2Ym(G91&!1Z|$6*mrkxQV*YDlqs<4=YRc_Gvm+_^}3ZD%N?1W;wWB?XrsJ4lxD&0 zI_vMwenJrCJ+Au^t^u%e!A61eXhoNxsmZ=_>+k3NM4&QGb*l8_wnb^DT=(kHA$Rhf ztHR*)aiD}Pqu=g)RP5!u!bbbU*uTTfD{o1^;!;WmINeQ#F7R?99(3rc%6Cu85b+kh9MU~8tD z!=0Mo2eukSpJRnYY3a&V3aM>%p{Kt;4K0nKmsWNn zSqrXHq3+m#SsZa{ojWdOLDE)}FtkixjTB>dZ#qif;4%i4RSX~^W@e__^p%-)8RjTw zj6T0N>bA8ZAE}(-;SCaSEXh++kk#tT(I|n&2EJLeb0oyO*t8VOA%vf$7=q`hgPj~D z5)57T5dg6Qzk?uu{r*{f{J`Z95BMRLw>IIkCJm2wOWDqnSyq&@7<}l9e@3C6sQ4c1 z<^Iqdj&$qM8IuuN`W$7GF;@fQb;(=WLLuoxpDkwEe0^*2qcz+PJI6wx->n|s$x$!E zFZBl~_0Ba=qj|J+a0}hx>|&amV0y?`j@U*;|J}=%!w*77O7d34^$}>kk&XqMnL_Lx zo|{1G_{`t~H(6m>wj}J+5)+vOr3T_{c>=ZQegl$MBh^U0%4sS}!D=Wq`ihIKAECsj z*apbtnS4C*HuYOB&Txy|9Fz|Zr;e=fq@k8hM>incOyP^**sGZ^#jGQW5+CUxdG`FZ zo5XJd;J4(Roe^2e-H|?nN7wOQ3z8^X#>`MwPHVW^x!#b2ut?ZYac<7-w2y^%^PWE} zfDb(}v2*NSh+Le20$;+=zg-{r`PNwBM}Q)ScWOF_BWHfI>k`gx0oe0X@=8*AFOGD> z3xY@N(#Pv=I(dxfap)HbHRfSpB;#~{>@IpBr1*Ze|4UnGK(rDPlO0HGt%X>$QR~@p zGW>wKGtn6~h&TH#?w~Ab*JA@SbV4IzHg$oTK`U_0qiwqiCJU?SF!Ix?28o#~2Luyc z$F>b2-sK@D&c5wEJ5Y~Ty@5>FA+V30K0IiM#$A1UGsCF4XN?=_fi`N32I1(nX z=-Dj(OYUqiK>r6yVwqXAC2H&CL+{Tf;U>KC%q!(2e;9Z!aK^1@nK=BMnH{k7pM)+e z>osnuyH>6>5)ap$BdpzI(V6Zb=WW_C-`nA5)FsJT9Q5y>* z<~p287wo8hdKvmagLNKAoW>$4l+35b z?*JK{9gQsRT+Aa;r>?JEy6|iZi=Rqq5rbu(c zstbeJDZ9|!xAqwJZpz^3(92BvT+NxVA^p7iGHilaMP5+~^=kigSF_3d0w`^?8UY7S z+=OuNBrB!5f1$BqEZ^z?C-y1E%(DsjRI~|^6^6nO zS6Jxf-JY$LBR_h8d5S+R#K#>=8Fq|U588$+L3P5|g)Ffz@`Lvh!~^wJfN~$2Yw>nU z)TBHI(I$LBPx%dK?pG+7?tDEAOx=rSZ5A26+!DxYEz}9tz^rGF9V-3KjS@=$SdKtp z591Q(0AU;}#a(158nvJwSkax$HT4}96ec5Nsm0N$ym1~!Hl}TaRywVF_w>$elB!_0 zBDf{{98Qp`Cc~NuOXS{%O?lFcvw>R^Pd@6^n=uw`J)L?1avJ#HxaIcY?ZctXT{HcWofTI2C_Wp%{d2{ScUfLhIxw&c5`qISwv{$R97LMHitM(FYfd| z$l`|$t{HTt7)^~vhQ2Y}u1(!kx*bYLP8QuiK}(i)S;9l(tIJiWQ!7aBUTNF{*Qwq) zcnz^MznHp3j=^2In`Ia50ArmeYQjJZ%xAP zgEnM;b1~!9VuaO%$>0p09$M_QyZs$?(@CjLtJ={ZhA`KoFc{yPEUKm5p97q2swwvR z>`Fd}9Jq@=%K~`}k-l8nwdNs^LCip+UC^1X=rOx@F4)qtQf!m?9JIlXxV-tAq5vl? zK)*=C^r$yFE@VL8;pEfJ=t${1igBxD$!B`MAU}q-5NaG)3Ib zpc*_LFLN25Xu6m&#blrEl2U(1wExI#pN)`pccXynHHt-FFT;+DcC!40X(#?fE48JO zv!(_ooq-ow31nyVTybfgUKpJ48*)I9Z>qrS(h(eb{VpG0MP;|Vq&j3=yvQgBz%2uD zAH9G2sv&$C+ED?v>5V3?Omr#L_AaOT96V09-r4WfKU*@>nN-==8nEDX$F#;JVVB=G zLnBDBMQTv0x{cXXlTEBN=XR8te-OH-FfbX&CEP)@8X`u1)Pu=KBztQEIScMm^mp2% z#BwNirvsLy>ww&md3{TYcBcNr{-{N}ql`18OJ*sH^Suima=xo?{R$UhIQ60_880K$ zWQiF<*D|RQf$uer7cmuOIXAGm{khokd8Ezu+xA@KkO(#5m!DMowO^?MgC4^QFWUhTsyG7G==Jf8=fzixO) z3$ENWcocItl+ z-=BQEp_UTXYa6E5qAv(pYH?^cL1aDd&^GjLif$F88rMB?|V<;ao>TXABzBBS;~!M&mgMO25@uPVbUHi*t5>m}S&sP>?<~!2(UsaJ)3$J**;o3q;((M~9$2JD7IxRZ z{b73Xkg@=B@tYRYNzoq&Ze}#mNCh3|l8?clF{PK#uu()&St%a(A>aiAG1+rGNaeFz zQyfIbZS*3L0VWN9j>}EoF&m(QoHdvs)wByurPU*xO$WyLSPsk+Lq|Mjjj@NT4yOh- zp*FXZ7G2M6IRpl}&-XqoLh+^zWx|}k1x5goXEz9M1|f6qI%w)#3f}P29jqCNvss)X zu_J)9=qq$Nh$F258dRQ?zB~V!Ra-LIaGF{FoYUs00KDtXSS1VY#<>|~VJoF1fORWA zW=$>%41Evt#(e>b|7rZlhcBliJBAItB`1BcgCm$XpZlAd5ewR;2$PRr3*&@LVhWRz zQ%3{ffdc*aYIInNjI&SWp?IW+{74ku3^L>{p{acnP^jVwK~-Y(V?F9eTyeC;s zTk3tbL);Emv35dNk^)wWUgo|;5)^B4;|$fZJ&>r2tgmv@Wnmv%TNn8bZqR?9b)oYCHYP5h4%T{3e9T!x1O$6C4dW}n@c{9 zc+8FP%QZPZu!-|~tL=1-RYv-ijPp3JO&3)EX-{$^r;iHSNd;@_ncjd^+n)x_k7(d{ z?33=5ysUhz*RC|B>-jKm>*P&Yf|4%nqRHDIZI?hix4yji4OsLI3f7xZ7} zYQI&1za7k3?irOIPPyinym`||n=s|P(~d3KK?Ge1+>N?#)Z1BwuJam`F?LNR3EnTv zk#aDDkT!xUM8+ye$^5|mAL3iG2!j%hNO*)V! zUu-s+6jpktEBKGIx-Sm%5We)Q_Z6B8>=02jUYAir>*5pv4{3djS#d;4(2Amxm>4Rx z-QV2^Ub_}6_42Kp{w*Q4Ofa*P$0bC5yi&>3wTX%HVrlKncQcm#!8XKnaC%smQ@X@A zor;Ovmi>vGl8kwX9LQJmex?5SI;Zal(d;y9r{LFHs+r3O$A=W$tdV)jr9<=eE(00q zrw$2OkNOP>+?%N0skg`6=_tL~#<~oauABYVUpqzq$c|Tq)wd5h)KFJxM0k+)XRCU8H!Q5`D_y3WiV=&lT+&fmZ(kM>ISOZre_lFic* z$9g@hhk(?pLUj4*{*YKkLb{?x5i#{V|J9P`oggqw@5I6h4@j{|ZuFf*sb&-9t3*j3 zGMdhbq6pxF;L!dQ=yLFlP#55nv}cWKyo51DJ`l2J$-_(ytlfUTR$`(U1|3}Y<~)(3 zmn?r3&V#Jwr(ZAcDguZh((f2QosAH_U-3-po#7ZJCo{119k#ZvI>I6aqUV@$+3D5L_4`Yv{jZT%X&k&Se6N@Fm}=cKN)uXm_B6vYLHl9^JaAf=H~3?1sF* zt$c+zO%4-fPb7$Y&R-?_M36S&8xPmj^1atpUs?8T22GUk%hT1*x?uNL&9;#9Bp>>~ zA15cg;T+XGN3gCvjd46H?O&WKNes#01!HZ-T|Q61WPxw+EMP3hLXr78i9Mv6^dV|* z=);X&J6Y+^QVvgUa7*;sigCtZBF*qY#uc`R(*@>bQ7F7Isi{NSfvS~)e~zdMn7IO^ ze`-7$-$ddUl9z2l=#tIwcL%#DTJ#3{VF$ee@T2RKEB8^Y0cO-oLRVuZX-jDbpP1Ry zQL)ZFd9oLPa>*J`p7kET?xq(Dow<0sF6?54MH!HHR!w0nJEf-bp67~Xg6QRURZr~c z^p0y@P+?-bkQ0Sy+v=jhkV$Ut-V%}uDHGN{l_;?!rMZB)B9w=%_ZF<1+$7qG!(4kr zEBG#X=lYv3_x#=pHeC%uQWjg_WdBXOkvR13g_o9*B>ZCJj?Y8B!*`B{Ppx~kcWj!Q z?Tjq2gJ>H3xbG<1M7KZ9PpukW5IbD=fu)&*@f$ykXCjs`u=~jeeR%qSHp^OLt+{yE zF%HVN2D|Ff=i|vTqn7CQ#)q=A!e(S@QFVNm_Zh7%Az5%1v^)@3gmwc=Snm%o?{J2^K{o-3ZG+g8T%29huT81kt9V z%EW(uP8DsZ3U?pLd6{+{UuhXQclw)JXqa{QiE{B*CpEcgcp6js$q6NXMmn`dcW0h` z`wyvPj2*k}51K!0&6p1xeH|#i&@mLXQn%VOQlO~x@!bOh_F8e{ASuU!@afS`f4wEK zp!x}Zus1%`3s?M!me`k2Wv#izK|pN{c6{pJq}HhOFT8@MzywcoBqiY53_%5;uj{$V z$!CMy1KLmLvnnjTeM=07o1+)Um7(~F#cN#l+QZz`X}h(n&Uh*g#`CBu0@u?8ySTfb zH|ioJkS87&kgB=qwfV5&8@vaxY z#hD#t`oRJQvt}-Pup{1AKKvoHB~f_nhew11z)dqSFgbfPQi=5SJtaKov^OuymH;LF zD(8V8_`7pDj#+NfOW8W|7v#xz<^rehB zg5trhd{>GIDE6#({F#~;x45QS_`;LoPo9`+iy=D3akGvlPn|Nf;A9A6o{%)To`{LW zQdjPC$UI+}KwzFeJ~DdR1Kzs6gM_MEc?cWMOPa!Aj}P}7Dc8a93;4qxgV8E=n4Y$Z zHp*8??gRwVj(;qST!)Et+03qQUKRUt+l4pioUmE#ZR0%r{$+}(+NNmJQc)5r$@9EE zphc+z`YzTJ8K)BO)#o z^OIw)cto$4=q!-@M0RrX^D8(XIGmYuP%vgQ`}VOWkC?gou|N%Y`9V?(>sRw1NV@pvwY%^oxG@S3D{N`ArACCiT-~tK@w?gM*yO%d&&roB2a0fee(O zio!%*@hLlut9w%3m>VUnjI21Yl_cM_rS#}UiCMXXo4)_?J3pGUn!{@@6P!tHCm1JQ zd-QG5BX_l-eRd!s-#JO|EOR^;D@kvAumgFraIRe}-UdnC3=KpMaEStNlImB{ja=Sa zD;41r+m&75rWq$%qz^?dHCcwDQdCsNO#W=n8&>e4*D~t;puUF0XwNF2Vwgtr)qd_$ zCf~HiH={j#I@Uy>(1Q@qyXE0>t4gqOCV9(Cwx0E@)Kx{7S~azvUhcZ%)m8UCbfIWo zr5DxaPJLE+eafG;-9rQHBlt3>l){b*FuqHf^JhK%Bf99D#tDG~4<*QEP|VY_G{AH} z1)WW@TkdD4xq0iCyqP(*(bf|nb4h338rm9AEXI<*QA@jPC`{AM{K~z|306OInlygd zz1AOXR;kcknQRX7&L0e_9kBzxbwh?QUX>o{Ig7Cc!Zixyf zzrw@~^}Gllll6;`zL-&Vi}kw65~8YXj+@JsE##FAq$?hFvNah8n@GpTAx=h zEouBdotrTuv(aQVnyLQWDY~e59g~10R-HHSHbEg<;v1>{MdVv3Aw|wr09Z$T%Ga`@Oufp!B}AwtSRDqhsJzTce+tQKNDwSH+I% zQIVkB4OUc-Rr{vjv7S?-r^fkLJajp2U822|J(4!( z0g*Z!h&J0ou6s}qMy0=#UrDq=yn_o(UE-$H!dA2r7>N}4wtNA} zD!aH?QP}w6=dzY4%DvolPwK9n7}NfhmHJD3S>w}S)+^P{j^!2Wv2!gYHCT=o&l1r6 zQZEm3L;j}(7;G#+1N^#?ustOqCh|Dgl0IGQrDe1yK!iKYtb9LO z`G0*42{K!eQat5z(YTZ?A+Ke&liiSUjoJZsW7Q&$LupTuCqJ|pfY}7b1|(#!l6_h$0-@T)NIsl2 z<_MZ#McUnXabzpsSHI=Wbd%8%R997~tZBCJJ%(I^^01jzdPj6FFGv)3N|G1Z%q3HF ziSMHizEMmHn}IAD-78I8^E~jmMv?1T{0sf7y=K9aW0JFGHf|o@R6?c_kgIYE6s7#c zA@esM&&1&Q9t&?Itxa}@It#EfmDcW_dzeOaBg^pnU3fmMOqI(dMx`f`@sX)+OmxOe zb4e}IR{mFXqKAgj$kDplgCHijG?Qz|nS*xZ^&?^CIoO9`v}~JW8ytMB5{xB-b}#6V zLPo`bXvLlB)N7sIZsAR3Oig>Xf;)>anq1$De>;2&36=0G35P*iWh!f6)pISC(pHZ~f*xyZn+KxdD~GlDf`R~}Ac#S%ya+`uc|_w58W;breE*{v zO5e>3-wC3LqsmP;k%S4n!-*f3&(pp{meg^P(tO(&>_+6cci+d=+x2k=lEWvx67cC@ z&zk!UuU$VFob3f|?(D3GFd_qHTzPsCbTeFvFq6EA0IZD!8L1tLroz+2+IVs6}lA}I7izR03BJoh1?m`(#Gam);H5^zu`|eABNJ%-AA#o zA9d3*WH;_tk9U+)MoH--=|n5&&K!A3^vh+ro+XMU)u63Zzw>dX-zoWoi7k9!P>gnx z;)pA1r-nx-8PmW#3$h}0bAn8%SQ`2I_HN(Y3+honQj%R|iUaX+CSC+^Q5<`!-Fh;Z zDBU6(JALWO{#ql^7>T>HhXHN1k4`D_iKX_JoEf3O)z3KRAmS6>`H!d(lr2rEcC?ky zw!FWewYsuLPRzQx%9#n0cWi>Zwt3Cpy@Ftk*#CtXei2 ziEocR>U;KTYaYZ^!JeKnrFHxMSb`WuGp}j7TVR*fK!iT`!!vpk>!JPb_@LEH-DzS_ zEuV0u7al@88PC+qohe=|olq9AiTlplgAX8|^Cp)Bt=958NfN_Gdq*ct!fs!`!(&^H z+HyU`rblcYK^c9JFM>yqq3Dkb*+WQ-<-RajcQ`fED#0?;or>Z=A z4g`@_r+wh3bx!>#>q*VhGbq14wQ%PJ2=%??g0thR6wvN0yBdqMVdgddw3uLCei2mW z_KD+^Qra5pQkf5y!&2(>^o*!9|g6w$EQjvsE!haXY3gZn$afpvlKV=>YA^6SL=-YGj6^z)VM z;ae3wzMLLSWz3YLuQu14%3{#G47Erj6ZkHV{sU_E^)NAB`U< zAnTOhU`_hwNTCedjVo_$$H=(6I7r__o#WBMhkniP-F#^ijlKcrUZYe^zm{D(Ks?zS zGwE-C5v#=AdTcDbwb|89dG|J(XQ{_`^@Vy|kZl+8S&C)2x%=AltsE>`eBpbz8TRpy z9ZSS=lDc(N5s>i*Ka9tssLRMlq&Et9CalxORh2tY8rG?lodlT0rc6+Bvw@7;sY@4k zn!uR1$6G2vMCPAbGt3YE#Sf5z;g;9uQe+Jh;^_u`&Nj#Q_9>9k$SbsKN_TFSn@pSY zvTDMxqiu|%;bJ@3I}x1b@M^BmNuh*5!2kihUZM}x^a{V~bR3RgDVh5T`h2tp$v&vJ zqz{ioQ>OEI_@614MJg}ej=5{ThOLPCan8O8#>hzhNGv-_thiZ2(`uCyk~&Zga5;As z8xQibS@?M^!ZLTa#v871>!;pm3~Z0|8x`|};{r1An6{o80P~rvZ6)=CK_?oUHfqr7 z$^n5w;N;N__X-lF?Pu3{r*`L zOqz(G8r)4n=y`Krf!JF-@8%;%3#_HBj07e^SDfmA^IIRs8e;Zci?CR!CUpA9qhJUe z!cF_&$_KzpCpn?DIs?%<(2^ z4REv)&Z*iCYE)GtQWpSJRQC?gXle7___8F~(>-SCa{sj|nD)iX<-Xmgbg0p*b1E_5 z2);2#CkWUa*OqYR1W$ZVLK(rdWBrkAy0BT}dYqSZ&tc&f@0?X8 zn4NcRs3F~eZM-v=a6#1US2`t2YfkVGrFq1jWAp-bJUG)oBoV$DK4FW!9@&Srwr@fN_|Za65ot<^={ytflpW<*S@%?ebnuEy+HhY!qqoc;KrSGDgE74 z!GkBoHonx?A^bxOJ4zdoo({8BvkAM6E;z{>gt+fFaba_s$3^%gdNu|2$Dq{ZNq+pQ zHXHAvOm}s@`Ocy?+-)yg`n?aV*`BlY*2Ub(EE!&yhYb$F?k&A#0(5}Obb~-;S#`V5 zG)$xn9u`*ENDN7zzJGP3)x)>knnvwCF@MmvcXDo5L3PVZi>ecc?!=F0RCXjP{qU@D z95Y7WNSkYzt8sj=plL#NFVi@2$5QLgSq~(h{yDPa3(3W$rde@xQGbpAqn^^QX|e(A zl;_IXz>fPWX<6U8njAR%ZJE>Hav2doCt7D-C*))d+svHu!@8LcI%Me~w>&1oUpYQB zghzrwvSowBk4qA3Mp&_SO|4z%x{$a~WtZ%WJHd(y=3e+&jyHTD>?o;oXh)E&;~lkF zV1SY&R8OSu2AFLqT*MMPOZ-5yI03n#>+lH|aZN;wQ_!to)P46jF`2SGFrvd10-w;b z2Sm6*->_d*#MMB_)dTzB67v3RMG`>=;Xv2lO#Xj z&1fCIpzC=g$c`GlIDGpZ%l<GRx(A#&Y@{0z!85pVS>ds*s8eXvRm z?*}`dnnvLxjtXq^cgDbu`Vf?OzxF^B|$URp*0|{;}DH9^IjnO&gyiUH0s_-*5iQPZh)C zDjD;dqz&NR{r6_`xeRQV{TH6wjsCai>L5V*fubt0y1uUQ2Vp*neCJ3r%432^PM2|Gcc4z$-G>fFu8LC7NP^(Tg>$-udTc zjRRg$Jy5v&A6LR(5E#9M|Mw)Be_Yo83E02n@P7jK`K$NW!&{jNOb|EmTw zEWAMccP)TF9tTaaD#l`IvcDYJ#*5=Qg{r#E$J}-gQp08uDRoe2c;ZUSg ztEoGAgyH9tE(Wh`giE}6CGx9Vo~XLGH?Ba#H zzsblSAMnQ^26ykB@^y+1EPoWrzdmbU@I}BPJh}hX#lKzo({w#W_l&&%iQ2zb_$?FkG z3P2Rx8bHl1TlE@fOzAt0OowRuM~p5S@p&4wb_FiQeR_P-X+Y;%isQvc8xdVD_46m* z42BT1WvsFt^{o#1eGj<2=px1Z+I>JbuVlcua<=7G(W=w8;Q{-&Q*kmdlGHf`SLb&p zI^IA3UC;i$Ycs9_8}w7aHRj*8?L1qs{gcSoUsbq{{BV^5cmlViq@;|p&moz5XnmsjFMX z;^HF2s*iO>z5M#v>}*vnTl@Cy#=ymZ@xm;5%`U#`Wh=ggThd$n#M>G(q3~p9P5A8^ z4*AF<{eG&K@16_POW|d$)?4&&i1i7FBAks-AGuO@GFZ4IzPvcXk<-t6$n$J4GVZ=i zA7h(ZIJ=KYiWI}2be2j+18}^|iyDSJ{;79W$}X^}y{e7XAJe6U4C^seq&nZEGdqo z@kqcq&3C#|U-JtNng;ko&L!>MC?B@{VK%N!Jk44&ta(6JLA*d|&>`&{RI@rh{%FYM zx5DSTTgu<@RL7>Phch1?jkHNlNx7bo^85&heX&qi^8%B|7x_RohL?{hu>W(w<~KU=VmB^f!Le~{KtJ_4fVpf_B+eC;Rfax zem0JVAgLGExLi`2GN*-N58LPy!aQ!fR&VRA=OSXnI5*b&arCqCBrO@3W8%U)kJ|T=_LbK<9A*6U(KeAo!1>o!mxoJ&%+4d(M%z#CwNr?#ayV zRKJx~zqXZ|mQ0B&WXElnCuk$5*Rq@p7q6?`zn?ou-t2b=1A@UHd?FHrDWzhhWdgRZ zHsCR2=kb+>&{-|>;>OKc)1^AQxse#KqQhJx9>zoadMUC>fVz3}D~c+BwtL~;rp7)Z z3E-|4@ZIfJ$(^>e0Gst00bB*^pT+Be6JRkpi^whh>$qlOd&>jLsny`NW2TpX)3P$J zmRZ6O@nF*DyDIfqO7TDqpQ|qsGQqQbz5OLT{=6QnP8;N}&HJ2T87i5S;7rTSW%|^# zb4u|DuX_%}l}bc1HAyML&3^E$We#dhZEZ0j2f#ld&SG0>$-}eo_Q)TqY-bz6eiHWM zCd!&x3UP692!96-!6wZnMHQDbL$K$-#p;kb8oUs&B&Ae^w+7?Xfr3m9O?m_Iv{YzvU!J_q{}9) zOoDJ%!p(?Q4c}kD#{@u|4r6{_bV^=a`F_bTDPX7MipL+11z@!@=oyTD6wt4}WBP6B zzN)F*^2}W-StO(K#(v381KuWs1k&tfbl1}Lcr3LAhu8p)H4N}tGy=mAq|Apr~@9#Os=XyWM zb@^qlweEY)F~=NZ&Sx|IYA|B)Ajdpku0m(|ke>uPDZj*Q!q%@?gF7UlbRpDV>t+?Y ziln0(C&C9sWS{gAU&n+ND3TT!Yf!)UmM)Xpu!ZH08^6u(xO_k>1aE4nXkdzCp5uOd z7v1#ZS=Lho9)BaT(77Mz3wAYRUgL^>hdTr`ugr;!@eI7H7Q7zrR`RjoN!+v4H8tmg zGaE)UTJFFA$i_enXE3{Ht;I1V-|Z2V z&6J*7U1EubX;DkduI-#l{Q`T>Y@cSq*RS?zE;^i@PyhAUt^oA5pk zb4c(;9*^<@?H4h(-IoRTHPAkG=52y{g61?Z(L%fB@TG>uPW8!&6SRLU&apTzg_$8! z{;Gd{Kxxv9?4PJxSPH>34UTv0h0EUzSWAR34z9KWuVuDkW|*;fwfCUhWc1dW#+K{? zl_)Y|ZpHS6=asVRi>ou& z$m-YKqhAd&Q_ZJ#^P4*vI}?>{!p|6^*h&;VSLxSEr>VDAZu&f|h0ZiB>=q4O)0IKv z3XYgwh)hPiMh1ann20dxa?43`c6C3{_&RrYclTdqUPTEUcdlgzDtWK2x&Daeo;7&5 z)=cv~a@J{fuS!fzzIrFQdzJgCX@7O#Rawo(K=RD8NFaM4A*(}HOP$TzO)b91cVVyo z`Psj+5(#knN1wXC`5VUI$dM7hFKs4TwMPIKvfpyXsKYub`l-c|IxNrnT?)*Xr zhmv~s+BICgAQChVNDT_rhBlHt zO>*D$oeIMI9p1mAiIY>wJ1?(zdcaDjNDE{E9aukj(atvjq$GxY-dOlUFurhlUy2O% zowctMDn!XZBOyVq>dbf z0gv2jZ_fs>qq%nBE}^=-8}c5>@VI!*2N_p`tj9n61eW<)y4~$4y&fnMEpN=G@cmaM zxectL#z}wjiRSNe@RP7s5PcYC8g4kb>ZP%8Uw)eL)jAgfW?z)X&4JhoW$-MzXRxnx zXr7TxhLtrsdN{+q(&^+p#|4)fQ%*zoM(Vi4;P8YnvDp0AsEfompo7q;vlQNDVl|E2 z-LE6~zkF)^g{xqb%CzhbdG2}muJmZ<@{{$gLWw=43wD=L)>kh#`G87o(6Th!e78ENiKop16_{qKvWjha9^x0btn~CW2n!PTQ38fuO%iM49 z78>%;32wy}#{0CIU1sF%whoIPPO6>MK0pdK&12_AJU4n3m`Q7!-k3m>K@wERt*h^F zWK)xqid7sIfq@($8DZ zyr@bnI?T&hzQI!DjqBi4IGsvKoNbID*@tv(H^DkNa|1k_U%>I)}L1nhVp8o;)XS^^XA01Y*M&X_j?AOEx zJw?RBLZnvY@Mr+5ikakcUa4|EyN1@U{0M>f7Y&e5tBJ%?__-JZ_bJzY$;u$KO|W)h zbxTYDq^7Mw*onAY`4H!`15RUkL5I7%K(^#m;EYqzUyFNeM3g~`9Bw$rL zRpC&9>R~ONC0#co@-Ez&%I1IY9NRXJhh8D!;_v@!@&ADQBER)awZ)5#$pAc0dM@0m zJx2ezUysRs@N{DZBgvLwpr+cibaU=qhVSHabW)oh61P%?6xQ~BZ$$cV^0qsOJ!Gda z6D^=8-EGm*q&un#p49t0m&Y7*{(XJR*=pp)@*w%ULZy!S( z*07qd`qJ0sovi#!YIBZjC&d$Y_tCCkc0ABE`nJ7&^KnQt@oVL4wv?v4n>}O@y9G~)4%Qm$qGd4cem+{7d@r=AAHn^ zz~f~c-s>p(`{gf0d{|YG{xZ@g&>egF4I5QsX?l5pjkz{AR*>e4PK#$QOm~Y`Xe4C=3R~Cb-8Nk|o|_a9qs4ZyxrF zFVk%uL*rR~Qc_k@cM9TJa-DP0WOOlF!#|XR>9MY0uEL~}oKCH?4hU0OsILIMn&5^V zInY?fNBJiL`Hez7fM32Y@dNQc-;MIAhf+dvVUh*mYo`PdWZY0aQQZ|P0R5KS>5PXi z3%S1m^ipgsTLOp2W}j2bsC!5BlBAmprbM^Vl2-s4MJS*lapBzCp9By=>%rYeBrC{(ASZ|C9P`Dh9H=o_ zG9Gk3P8&uN@!s*M9X|99m?ARWdUIgy!nyHhW5JCihJ@oC@RBAhI+L( zkh&E|p1(DL_Bh~Qg*x!Nr#~UChif!YDSdAE`?T>Y3U{yJFOdtvEqzrNAOV$lQ5Jja zNN$Ip&z9oqBgq127!-1>+6wiu?jp{owtsaAtk zQ75$^nt>$d8eB$==@3++fXW)^qL*7c(>B&WvmZeDRN4YutqoCm?O)YY%NJoVPgNby zwP}E1y@B)etl;~JUsy^JViXl2tt_G(TpTm|Gc`73+$jTrtB?t6QF#Qox<$9P;*0&7 z;pa*b*`AvA4PCW}S4Vrw8lgP?u zQs{vo$-QxXRc@Zuz~tQ*4@uTl&8DK`;kPyO$R-VX&Z81s-qV%Kqik+Nx?Vf^$@^^& zHzq7j&SZ|1cv2fL1!xGpi%Xx!ZakKJ`kb7c?a1Q9cp!b7rTdF|TVSWHNL~uC6%yn9 zNu8YFANMw^5ED4%mYPw=e;@P455h;YM}^n8M~z(qB-FCE5THP*9$={friH<+HNr-* z;}Q6}0>I;IN%=)HFCIe%vi=MB;gswT$dk>*GyMXJK`5sYN)%6XI+Gpa+$6=sT6!Aj z>XAXWQKmH5yoWzfrx2 z@P5Ll{50Z_LS(HA&j3YEunRo>_gDL`H15d@P?j0Ka0EiXKQ=7POima01rrXGylL1N zBvD#u>s@R|e7b4-wM$vwOn}D!o<4te8mAc@XZX8P{;qv)V%B(OcYb~ewK%Z9n#lDi zDzI#*g_=RvyXa&Z6BQNJh=7h@A}y6jPpb&wn4W=wL3G)A0`&ec^qg}K0#UbzH}R5r z8!qFl>>KW7@J3V9OGo{Hgsrn9PLfnwdo`$;OIB7kz_D#S(8PLZK!EFqwVZy9<65_Q zGn03U4tBr~Mi-9aHRky8WO2loYzZS-RdRV4v>r-yt(}eMdoY^JZn*vK9_eZf?}1Tt z)@h9g6lrAtLRLWVCRGADWjzL>G>_Ct78LX2sAk9sivkvmH-e5eD+@RS<$$5YZ)P@d zh&~LgDw~DSbT4L8E!Xp2r%i!9lgrlg2u69?+J)Bs1Cl|8j7cJfipnl2_QC7yJmF!0 zDMY$bAf*x&{lOYbR<(4017{=}s7c}>l+aKw9z~U%HKVSso=wIaok!5;Uf~mt!v|@O z-=hBp20eW_n!_0XaZ8ra`TEPW#qEqf4wmbg>~kpE)cgP=XmI$mOfLp4)*si@Pzs>i zt8#T^HlIE|4>zU*Q5njwKI*`O3;DUGHpuIGZyyhEoLrygcQ8VD>eT$w)1w*{?t*;= z@6~kC5OKdwscoiyd3N*X_wWXKa3`NA{;J0lb7+GensCNFj5b!Kd#XC^;qAZsG{ zajX<<6k*sqX6ZO|I{xXvIwIp=1xhu?Ydv%N@iZP7+ueq5GGD!mK_%sw=9;wQ=*qxv zn$yy$F;~D~*Y{qL;M*;t`gw;S`oV%=eMuWUezA1;q-pAzBbn!R_Ce6{A}lF&Q*G_w zoZKi%>Ko5C+9L(PFo*8c|~H@xQ4X!=jiEG)MP)xQR4MZD7O z>ym{rOz6*c;;*ljw~WQad;%AIZy*%pU}M!U&ST+Rz^qzw8k>17q5$JzFFSn<+UID* zhGrXPt)*(8Cu~X)9hoF!iOqnV(UHPEZi7OK=R)f4wfw>VVr&2u!h^%UX)QyGh&QWQ z33&m{8WCzkK%|_rbYnc~wywaWfv$zU6cm3-Y3HIiU?hVu#^qvbf(rigLuDvEL66Ar zL;d{7GWnCl2y+begqx8Ku%vUWc02V!Ubw{3`Up*386FKWOidaUkj(URvMRBG8YWt! z_JW6c%zRfhHf|Dn2{Ym-M7>s6Cp0*$;Jq4rJ2kiS&tt*vbu29#DjYHIHrHW9JUsG| z;h9~BZicH+iMXdj2)m@|5ZNfl1FjQ_*v$HIK$?X8Cm9UsnqY{e1k(a_dV>GvhijyN`x z&c;F|E?$%Lc!{8LU8`zS|Ct?P#Gm*ZOEsnr!T}IL7(_|_!%nl1=eg}#Yj?sP4`KEi zV5JzM(j-DcC@YpcrmA?!&s9xQe|~;0!V09Ng8)$sOswSmaT5iOajtKd%Z{|N17)>& z<6V)?ANTrCDx1|FeDB0QSfJ_{OOj1F;Oym8Wsfp2GSsixfc-SF$UB5=u`9BA^FniP z@%=<8(GmXyDpZg8>r*++`@aex6#u*`Ayb;@+YE{y_EKs%1mlYPAg?qZmdR0#oM3%N zArHkHwys-p#UR?twa+Z`*F4u6P}J`Ch)5K+q)xjdTLsHgveBgLZx^3zm~FJ*>uYM_ z4RJD#)D^OGw{l-yNyffXQCTq#@iMj`C?pZw@hs>H=h0k=f)Vcfr7GPige_2FZA3V+O6m`9i0#G9`+R&R?2QyM@0 z)L+D4&l_+rH=I8CC`}WNC7UY6TFvMh9BUAAp<0ofpO%`YD(N<%8HJet9GW5Br8nXv zO<^5+izyMdz8>c>JH?3f)>!&?R{vd!{O4!wO9N5IHLs*k_wgF<8^W4SYamJLEJcf? zu5r{v-^r}ml@bk@+sB)&HSF`wm^l*ewP1x{1kLn9uxz2kr=~$Pb=P1fs1*`u0lJu~{UxRTDLde3Lq-JuMPuYfij5 zR!H_nWR?Qorb+xI(KkTbonudv~E1VVJ=<}xX+6oM`@U< z$?DtY&JE3ctVh4u)EfRD=U@aI0IX+0C7t+>l+BqF%&0UY%FGQtRYNJ5eb@ei3!!@+ zW}X_ywD9qv;v_c=d7#f%HEg@?baghmG;!!xxgv0nd0xk|N=^U<2xmSDA?MVYJr6$1 zQ8Bg`{jz#m4)#cF42&RTIT3XY^^li&lu8Q98>y*8W#wSS?_wCon>2cIcr3C4uEAc$ zXE;iM8c-c=kZgqTaWfdprQ;6!FfhZdj)3jYG=>aN3p(T!e^F44ME(|8lNyq@hD8cQ zyKAp!Yy$}lMDin9SIu`{(h#9`urW5t^OM~SYqCsvz%`+fk_#$!>4o{n;#z3mu$Rge zjrh)YH&~gk_@&ew(rlLB)!Oo*6>i!1Xs70i!z<(f9zL$a1`mJ=v=6>~!KW5sIAOSH0>U%BN+TBI_^ zIdh63{(n~MvyTcOl1jS2>IOY2a{P^#f_c%W?D~unRKB{Cw4^?!lr7fS1^e*_Ej=E&b1h`2P^G|LtQwu37o3kax!Pw}^_J+ZG^453*QJZWn6c zh&wUcj@{yPCb(8tbVSS%Ca0=Ywc#TSUUTI6eE*3N2-gpZcN9JT#`bSO}Okb}rPzU()JqMg?^jybY+0;}xJb0{Bo{zDS1~V{#j+`>sNJCFH z9CMxes{3jG{DuHpMrV`xJ9Ib;!N{2SaU3x!NkR-Ul2!P9m`qyuF-+H|s@p|mH9NyChde1&#`E_A_>SEDo?RzOKE2kzJ5%4BC zDUoL&&Y_Id@77VL-nERx0U zochJsEiVW`2!zEeZ@{OZJ5@T_LQkWZiqp){kJsv)g&RN)2JY{gy2$lf{x<^3o9u?q+ z{Orvu)R0LmkGF4)b`mv0OyvFfD{ZFxnB+cx$HkJLU+iaP^Zb`)A{L*>sh`rM1&5UG zkv|oM^r{tfO2nAPIjB&IbQq)saLb;QCJo0G%7lAaF;(@Ol!*GtVCK2(WO&p%tcU_| z-pA&AcS7JjOcdcC$>L+Eu(h0AhN$V^-9a$zQ-b68=eKmky&uR>uSP$RCA&rp@B+R? z3b%=`>LNu&<>g>0QV(kUhxiQaR3#kbRgJseFw)q%W;N;X!s}OCtN1NZb0cb9rqaW* zj{8*^iaiZoIVuRF^9gS>Zf27_qTn0wZN?g$Z?f^AOh=!z$u{c2J3Z!nE{rQ##WNDm z$pZP*al3Ii4ceUU zxJdxEyW_>8AfA=4BRTwJ zI-^12_*gRjlRDV;2g0T2vV=+RV^I~(n>Sq%z5NM!E} ze7$%5O*1%vcdwaf*1+%1g^0~qJWP$E-DpS0{;i9P0TI8u^wC#3w?m|G?2}4&qkCix z9Rq7zGh8m`OXalFapE*~4_OSB5HzAyQUVs6Zy!c`Zi>E^qO5l5;z@N!5s+ZyourCh z*>)MdZ!*rn8ms-8HkM3F#dPUtZotgbd8tbD`F%Kz%dqWcW<9Oqzuwp5OCt7%91WVq znsj{ltFaV`C{E=&Z^gr=3~r`7i#x(-p*41}A(5*bc|DP6q`8$a^_HG6V#1e=K53wy z8H_rSY$7~;9Rt2jt~9~>HLj8TX+?K{ZHHG{UU|gaqk!146O)At1#RuqNTn-;(Cd6c z)8=5l0qne^>UFsiYTMP13trxUa|K!vLHYGBSaeO3{q}S;b|}T_yhQk~#^b@|=IH|5 zhp+i|dvk5JE3-B9CZ;5#3C+ZGFN>hl#6LpN(DiX6Tt=6BTzvI1eXlL&yfRx_=C?7{ zDA^ZxW$d;hRHDM>j2jOv-!qvjCj46l?7#S^)rIPNnWH^c>^j&8&o6651nV+SxTIN5 zsjGx5Qt-N-A!2{cEgN!4QsD+;-}F7l?;)ti!bvc0NvW{KAyR#bbc^#05lc|Xb|d>^ zMAyfYW;*2>H6@_XKgxZX^c)S}qV4M3em}(|w$9TaM}|R0EWT&dVRCP3YFa=30^D*j zJ0L*RJMB~VxsFVy7%m-EubGij!=t;`G2$KFYjgMUcyRB*j;LC+j`5u+W2bh(o;5$6|*aR z-B*b~XzjNW@E>=_`O$hJg{b;h*$<}hPY4#mKQqg6`_ZcEL~jeH75Cooo3 zW4aWeZ*MOJY*LY5V3E`c8Ge7lSV8CZG^gKOkmuL6Iuv;kFO<|ZOaTyVgps30qfU;G z;D}?uy%subKHZwhz*G#Y&HXq%3&)L>5bYLl(^u*NRZM9jD@g9l+z(h7yO~sab!8smMeag~~ZF06f zO8>DSLipKAEBt|8`fOo({V55R0G05d-;a*f?4C=_|PQ|lA1FwEF_Lw z-;~CrB;XyqvZq`8RW@|x@xRzEpwm}HrKhm@my(B%_-QzmNXk)uIy-$$Ne%sH%J=2* zt~pNyX1?sNrQOz<|0KLwOLv>~nvWE%B6lUMQtNN(2*zPui z0!OUXH-qVXJ*lZ_qBh|e2wvNj%VyadUqU=T10A*5qzWz4Bq!CZQ@|g*0A;kv1)Wnr z&C99fXNp$e>pPbkep8=f7%QT)3B#+rx&>nBhdasHCb>!)g(_{I3f!SDrq$h-!`H^_s>$T zZCasZ*!I7>jOc1YR*I0ZP-`1KBonrYYRzy{!gI^Z7k~KAD`cVPNNhRpj!b7OLxC&*%(w)>0!tb;D zj~uBZ1+XW3-&Bx)BM{doF!T?Bq^y`8O0X1^6!NAa>Fk5=(D*EA33~(K1Kd7otM&s; zo%Z%zkq(LANaP5rQRSs&IV_KmJ@ho{s2rPaF60Tx?!kEX?w zB_zck9k~e?P2Qh~_a-J(RQK7s>OPr}+cc|Bv?gYtY7zOwoM5V@sp{(_w3*mM>%WSV zzyVa5gu;=@_B-*5ZYsica-8r&pCgj8Uk1G4yB!(QD8$0U()T*&L>F%1sLd9hAmX;E zrHbLFL_pcIo}7~dwV1ATMs_x`|S!(39 zv0F7f7LIKGmIUY6V6)b-9*X$?!M&4w05~8t2h7hfzu%y<5)2llsl}m%DpRmwsUlHj zu1Zrbdvo%QRW<85+0+JG8|YSTgJRY~M|E zZa3`FFWFV%Z3eg5CQDDv@{LNKtbSf!pc|5Kk;rF!ZvN~!N+qy?-Cc)aO`n7%0+xbY z<)vCZvMW0<4V;!rIS=}(M~{`I@z1K4&UvoYr6x zuR%7TJ9tDc3yV(Vfnz$n5c~3r&Ga`4*u$i6xofQxckHA%MlcJ@QW&(}Fa)1EdV=Ui zQCU#L{vhrkk^t`u2}zClJ0j_be2+e2Ka!$q)0QSF1Q)-kDLc~{Rnou>M~TE^2J5*k zaJo}BuZR@FH*yCA!7@+eeIHlAdcnmx*C~SyQ80MEoB42AsE{Ve?5<>U;&h_%afDn6 zJ{-29^(5k_-;M;|by2RK!%_9G0(kp~=sKc6t!(zuQ{%gFD9f?g22M6t1+0Q-n@rlU^TB@#fLyY2S6p=hqYs~4ojXx24)x=8p z*C&EhjQu0A_9n~Feb5NlJ1^3BCkVauR~rEAyf@XB8CxuaH_G6xn@CvrJ^@cE9)1@M zs(pPS*Hto7C;iT%!z0eE0y+7#G{wr>7`MW@*&w0fPD>J%daLM8OX?{gZSs45bBL`40nQ8Uz3z0`n_Ig1K}EqRg~d5+c|cZitN=zA1o<`DY4l zE@NLn_A=QMklZpE)H;>AqCd9l>tkZ!T{l>e2R3%X0YiWDlcnQ6X5CLz!MsWCy*w~c z{%*~@SKCAC6?7NTA3+Wq7K?Jb))Nb5E9~z@&M7aSjN#vvE*4#ULv|?E{{1G%EIUV} zbu`ckUWGD7NMMRRoc;ilA{IGavl$8QWRhL&SD{ipu!V63pnA&ems-Ku=7D8;^<28N z=QkH$)ddf#ml0JBG!Hw(#g01ZD(FtXFFgw|99vq`8|NO^X?Q?>+WZSfI>mKTYc(6Z z!x%ScRR?nIyOFp#&JQ^Du8ul+HKh%or!QsL9Tf;6T56t06I!j*=XRq(`3H2~zl7xy zqG?hWJRNMS7 z!w+6Gc>&7EsZpC)gmEV0?l|v6i)YqMVOPGl;P%f?gR;d4BVch8ktN-P(4KpE__}+g zy2g7MSkM%ETO3w6tqOu)NVFMaj2t*q}Kh1 zF~ieIant3#feh&?S=afKfB@9;v537{-D<-)Ia|Zyz6A!ExMuvk&|;x&+gq3Sb&}eO zvyGWIOLow<6+AtAwQ}AU>&O2#_}{uRl1C~m6m#rv?@!hmY+T5Lsdc+Sd-TcuQrR@U zKRGTd-`lTiHya1hBinMsmL7TZYK1`?G5ud<1D|BS*QSvPkYMkky=Uxa41kK!tv$b^ zf!d8Y1b$!X_F4N9&pmv&D;tu4BRD^7*aC@o^IUJ|iaoL42!2`RU1IiiJOdjgAeX^o9pEwEs2Snl`q(L0!VedJ?ep; z_l`tSX9of+DlY=K19avWTp>QTR@GP#_zg-P(%zhOu10&rmtee`F()ZmLNT?P`i(UW zgTUvgD54T?&E9T<&JH(f=i|kn!}XLqvl*G#+NMYa65Zo5X)QZaUwfiNuoZLpc$;az z=!-R-KU~WR;>SwBnbxM1Kmjv*H5*Gye^F*|d@7oIxOP|k^t`z0>z7qWi!bOxw^<|} zou9zJ!qQ92!9(T(ymJ?sjcqM!mu|46QxEH&X&jbuktijiOW3rEXB#C9Hl2!89qbyo ztw9r~!^6mHWL!2WzkzY}*UG9jSEyqHyp&olAgcMUQLej_{?*{&(@SMjl7b3LSG$z4 zO|nJ#=K6%s%`}ZlNG)+@+}yp3F^Y!MBP$n2V)b+TJ*0KNmb^U88*<7anC!GvT!97TgQo?+Xrw4%G9` zYBKLSmY&hGsq}26`}vo-jh+DZjVd&VE_D3ca=C}*ml6_Qw-&W|(z%8j}2?ayqtF!^qzy_xO}GI0C8kEwhK%RSu`BY4P8zog1Q zdxZq=tGF5j`%G_bw{_y8h{!6I=N5s{WjM0(EG~J*t-rOE{}&6uCbg8kt$9?xn({T^ zk4`1KX}6dwu+GX_V4mI_`h8dAIfgAMDsnbjg^;_CU!SNp#ge39DvcK=l>)Qj6UtoC z*pFnhEE9tS7%la?^49MNE8e>buni`jBo3=5HD@R--@_l&)C>tS!uH*F;0fB7Ec)`v ze;o|&c(~es2uX(d(f05KIv0pOghGlIPa|JBwd76}7SdQj=DqC82RVCXv;5(nYce(w z(0hK+fa$U@xR$pZ`cl|&^Uw}YRvF3CI_y6M;23xNcJ$ak8U$)yBBPeOalaGcjq9#kkF(dZ1nX~_#&cxsV;t=Wvjp{*q|NS_dd z>}$bNV+~$!G2MLWiFdzTk=h6D#~E~k|7H}u%^qFgtTLM5)ogj=HDS|cs`dU4va2xA z&RNjHaSxwZmM5HNvfGGKdE26*=N2CXP(Y(Yt2=+pq7v!&rwRe{^7jj;<|=&88Tszc zenC2hb#bnlp-}%My~yIOU_K8j+<4DDGR>~a?0*V!D?bbGxzKTK)xiIKe0*O_2{e=WB|{h zu;JlU7^T2(B;vDu4>X%9tw5CHlo>2K$^T-SH}3wZEmtx_cc$9G2Qln?d#FDF|LEOYv~5_+k`k5w<)`# z#L%j`H@jIUB`TPZ0HyMc6AzYNhR^0_*5m|?b*1ndwOtUukWd(QYI1jFov|{$?qsZY zPSA-AMlVg4*%vfm6g`IYJz%eq=ef(>E3erCTz1e!zqX%HNQTz|!O8H|_tzqwbM_C# z;EpwMrn8Nybr;7oww~!)OZZ^GVsr_@yNQ#L1M4|%B$DbF2Ny%Mg#^zk#RMh?*opWp zlnb^$_&<)PS7hrV!{yY)WW0d&Y__eCL z0G#eVCd1{r&(m~T&Sy)DN2SDf{${SyG566v2WRf*cWFelDNlAYsEq^Hk;aE*`(U1f&ww+lz5{S(T{_vsh9GX=w@fBG5pw%Br>|3{B^i$iR0C^( zZ&vZz44WX0X?otrFE2a{&}4Ve{VoF+9=<-r94$-;~D_)R;|f^Zx9!!T5m3|MC8O+T`<-`I>jbV8-%GJB#<5gYXc7SOKyB z|H~LlMIKlYbu05Ut4s!V^m}wbdSaXpE%X(n38K9wpDuS^;=_EJ=f`de&>S&2RebnA zUq#n0j(#ZwINj(pbIt=ZOlR&Wr_G-PS%X&(v}I?f$3iDs<>`h!a1oEABVWzj&?`SExcgT#RuxAN2eHE8?8;>UPWm0;lpw$ zO>9&mqvkZP!6b9R>5vd;AvySEma(ADKM>p#nDj>p%qzOF=fA0xGaeZB?*pvF5)sBU zAF&X*k_UkbzhH3lg{s%rcs)?kXwA8Hx6Il;6JqSLyJC}yPg836A{=ot{%OMP0 z!#%{_cTY(bRP}!7b&splVOY(uAYZ5ch^q3Qz(r@eU-Pd;_fu59oz8gv!OZ)6%POHvKHJ*3MIKbG!(=XfK!TttIrk}s4 zzL`o^_9kc}^Ua$a*>I4L+5JyRNA`|7M^G3$G|J*wczIQY^67U2EN^Nn4zC6p?)iSX z+`ea@k{gs&R_-_(m2)@J!J7Rq7)0AJ$1ezB@0@jD1~kiS)9f1Z=VD_2EuPv&05s|` zBe^VpF>u&0quXS`ffltfb70=+bL7NGv$J(?dF=venLF5LZr9-#)ArRirP#w=Ytwf8 zLvFtBTanT2qU;=%o^DtUVWF>6ub@34HOL;U7c3XNcW%o#auaJ*W#9_v@o{+OO(s;> zcwKmj_>kFh`lPmo?$?8tm&{8<^)Yg4960)b05~|2%CZkYyR~$$T&r>!i3_JIq&(uF zys z+TOlh9@Cqe^5Hiz5I)f;WVnX%v+v`)D^&uW5l1EIw@9>z@J%;~T)zqri4KORQc_Q> zY^MxThF1c_@+0w9UpcN)x7GW#lDKXg-7~qqhQ;#q9$gW!p5jzg-{>lQIbKM+Do$L{ z@XUj(uyL_~N7Cke)J;B93*=Q*+S27@zm@%$x!}LcIYVl|hK?U@AI$l?RuK08&}56e z;m_1N${1P+>#y4JLwG2$4mW+Uzv;c@z*d>TZGFROn*uErf3RaVg>}QbVW*A?z^)i5 zUQv%c)b^b)jWF=LvYOgj+j#4wzURCnm)oK)IWgWmOg59lD86HnF4eYrWD@6*&Agpl9@;FDcvid3j z?#eJ~rTZW)-V2#Rm-+^IiOy6-@XB1fEVkHzO`I~cl`2-K6=*}G6N!Im+FUY|BKG}e z06bCo!of0iOT)bncP?G_d6&~5{JWo8f6k7HVgj^|RC_*^gui1Zz9@{7_W%X4k`+qV zIGkfUty;rP0-ri&_XZz)m1qxRVP8RAfHy7`J z^ucuHj;KrRHhBybV3=xK7Y)I8*tyuvw;Vb%MDhFCSx4y6s;NvQ6C7#g=?|wv# z4POS6hr2CUHZ#aqbs!TDk~dlm9ofm1ayDIjI$@HOWh0~CGbZ^R)&5&3Fo^m^+Y<@Le)2WPgTG@6-TLbw~R*$g*J z--4&~y@IU)ja^zmQC2)6FPn<`TibzTP`%^%nrLarkT?lkbrOE|-WO^P4sNc{`&79p zHJKyjC)Vi^AISufn6dC@y_M=X>nMhT$}`966o>h5qL|+{b|zg}qBKBWTG`ArP7o3i z2Bv=-dY5rr=TRE6+@eXYfBXq2qHb2l7q9JQj|;zbV{SsA~95dQoBJj#DP zC~F)bzKzPOZ`c0aP$0*kb6VWG9egHQkYFM(gnQ~tIgw9>2&cj%z4yy$He+_Tw3KrP zi8fkI)^?|%!$d%wwu$%}un23ab~$RB-0%qG4UcBn03&AL4p5Q>d{hg+DJoc?A>tDn zXc}xelF-Zo@t#OLsJo!RVL`PPuUppn$Zy>5WL?GxHG(R88bcvkA-iN%si-s_mDlX} z`!hM)+{|y6cZa_AbuM+TOFqy!yC%qs*%u2~r-@m=b{g$Mi=fe1b}xyXl0s^=XE|?P--nSjg};Zs6RuC5hSW0ROCnvjr-!uD{$iV?&o#Z~yq#3HQ@g*sZ@3}{kGA7Q4 z(&Awg~oDqvcxacS$@3%K9)b_)$g&tqhBN&{tEDsgU7S!Fy<8r%Zd@hlT47zc9xO7xRcz4M19N+d zq@;R5ln53}1mT1z$uHx$W9;>x;XiKPe!-&2U+Oj8y+8M!+(o6SoU}Qa3&bAkwVXPx z`oD%G7rS`aJK(6!Jl-mEzFgNkxYt|r?tA6gSg$Ndi48qHjlBP}w)w#|-%ujYsr0L8obu;p60Ug49a@|Owk#E5|Uod?9B-eWc%)CdX4?HwbQFLgSHegoL^>2 zH0xZxHZJT-6+lyfsI$Nugtrz#a&3!Zs^M-kLHYR>&H3i#iu+=dPO6($Ge>p|X6U1x zB}uXOlOu2NQ=bnJ++<4qI8keC8pNMILh0-F(&&%j*k^GeB&l)oL|Ok7Qa_csI2xm= zx_N&+R3@DDJm0S(44-RN)@+x*818Itd{(jE7}17|8`LErpHJdT_|6~=Ue!kJ5)&#| z)8=M$faqi6+$J$EOJB`R-X-L{-Vnu6n#r2Mv?**s?yjSO2D^dM5bmzW#b$ z{tQ^@IF^=rPMcQ$w76aP3wzT@rQQ^2)jzlOVL(G_wBIc0d~y80?j!vsa0sqb=69*T zcW*jV^0|m55r6N&{nXWA??Tvw;fiBeTE*k7WLU?6|IV)C+S$l^5RnZ5$r-5DhpWIh zl+=2KOAB^?kUfRSfLrBkF?fuiOr2~i4 zrlM?Shr=8Y$xWe;E|#G<@2VY1NOX2{AHL<@Ke)V+xpd#v!yC$*G#(Tsh40U?jPH3-E_6s$t0sV134CV9drge0`?e*L2$DUkxZt+ z#!ueb)(uUpt5=OF*#5ZyqAwP}a`OX?U#x`$W)hk?>=lN4_K97f$~b}H5qm2X>mV9_ z|7CvUo|9z{QzT_-hS|>ZJG8F_ zL`&+=9USmbZuK{WQ?$Pw63F;|LOYLjR8dW-57m7fEd$xr`)IRWldb1-vaJE9DtS7_ zl`B+0DlCV%K5lo$Rz|m_T5s(0_bE5bqMJAxz6kpkGzzPJr+qk1j$X7RSn(*fugiV* z=;a%Q&PS2|ytUyMEjC_{{+{YYF+~XkRamDA=PDta!Z=}@+gN(m${TF#Zp<}GulDCX zcdUeN5npLsDnz`>fA6Z%^)HH|uS_ijAzxQQoi6uKCftlRLpPoH3a#eXaLzgKM~}+$ zEYZWzBeUEByB+w>MsmC_@50qMs_`t5vz?HcT(-8#a95nXr?;hpE8n-g6~gV<&bXYcB!vQr1!+PD4I-HP7oo?sROSp%*}T`jBpsV zhv-$6jNRCX_t+;{i*#!OGlU-a3mI$D+^}{7+m&t<g{4mpOLH2m)Q*WZ% zvpbgFJpSws;QpgPi`QZXH(oz?LS1x%U=(`My5E zU49Wi@?0@{<2$;zCofZAx}=|G&ze?BH^PbYNRIGYa>T&TMV|JuGWEfY^kJ@Ae=Vde z2eX$_<(K08@_+ytDpft(FX(f4Yd>6Vd;)+bd|ogNXTWkgG0(K5O|sR;G;{$+CG~i> zSJu_HeJ%O;s{Ibkpeu1J=^~Z2jW6k##dETmTxK;KSF7EHB9LH&v+5R0-IfmGGt}|n z(nq6oc|$@S@Tr-{6Vt;ilq{>(z6nR?&70nNsyP$G0L%Tr$qDxQs|48nc;u=v_3v-A zvp8_f*Z+_=?frGTLoPV>q+JCr%es=|0Aa%XC)}VYfj?={g^~>z zr?vZ=fIjvbz;&Yq8dSX(EY(SM7U1nk(;|Rbo1>Uai5r|6ev5b5pKxq!bX-ebHB|kG zAVd~$x&t~cYw&h$p!Gy4fVLJSZ#{=Qd!HoXw8bOsw-?f~96-fCGaYVP7ny~Qbg=JT`(d(9~cvn;QU zc7jPFGrD+RIx2wFmXp9<`4SRR#;Td>n(EUT(xJ$_+_dt9`xv8XuXBFqoervLIp);2 z0L`qNJ(({MHhY?|=2f>sd4XkFLm}ugDjF;!X7c>B&j7nY1_a*Cy^Rz`C-$caPhUvB zz+d+aNnAyC5f7Xf37B=lLn*wln}%CJ7C>YVN$Vz@kUYqy?mX-+)%bSp8Z8E3K3QD!HNxxjPT9hG9)Oi% z+g3Z7p|xqs@jF2byUotod?|o!i*{1tJ744|{nwfDItrh}*C?vRi1OEw5|AL~cw|A5 z%I|F4xT_U`9a)=0KAZ}1bGCY1Kg>Xb=>h(ztEuUP;u7qg$82c|uY2Z}ms46g$U1K) zF-P_;Tpcsyx+}4bo!;LO;8~p-%f*tdZ9F4}IPkueJ%1asQNUVoI+a5RUhHN@mYGCO z&eNUMTLV_SkCal(Kq5O?McVb9o;#tITiM%)R33+%#-Y?&8TOBp<@z4Thu?CF$doYhSzE!`Z1AmQvBa zGdx+OLNc7YeE3J#CHw&J@#0q6|24bu=8~Zo@B>#AsTeOyML<Hjw7JNB}+v zW}cdLnKvFRavdKK*|bU)ax34eS~GAyWs9r^q=Az#>uKMkQsXYA>Gj3dx&zBCVZegs zeDPG*z`ztBER6?9AVb{s_m(TB~qA_Ls^`R&=zs5&{%_Ao2J1XztWK1 zMc*y~VY>*g!NWPrl2(bcwKdV|3(DtDg)6^-pWQg`qbSM@{$ zvo}B(i^DUsfsH7M3TwIdPOeR4C+Jf)o#!8o(AkuEoxzFs9Vi2S)uI1yxIJ+IoWMwT zR0_?nZ-~(mNZB#2Lbx)^cJ}>%LB&$8uOjxT=zN-GC4cPP_@{g73I1(u4<$(w(i0e z^Ba-SPch6y7Y7jr=u~YIJT~U70FFL-IMTCy?q>ks(OS6kX|Lh7W-7KV&ERZ5aj7NX zw@vvbB{2N>${4DDStv#0E+ds<(pJbdSjXbgioInv_X~Ilh#RqE;L4aOjoMczOI7~E zC(4Y?8c?>rBHogoF^kv5p>TH(Kaba2uhbC>pyT%jD~+ucps3y@!xA+9jh(zGOQjCT zD_vEGQ0uvdZuArwMM5RP%(}5*p=ihLh*fcNs^WRZ>qkvN%1&;kmEmg@t9dl#A-J+q zi;Y`Z;xNk?>e`eP9R#+iqTwwBuC%;mMb)!vofy2eaatyK7Om>yr2N9dn<%lR#i^_H zrM(g0$5yPm-#+8_XK#G8YfT6x_^9Tr(^n+D8TOdHOJKzuvLG1k{8{J=u4CiKFaYGN zYT2J-8JZX6HIH11TIbNP>Zw185hC;cwR}wgvk$E4HE*0RWMy_-;wLDz+3vNKeCpRS!#-of6?0Kx`78vh$+fWOB2xnqoH1ADjX^J7 zely4CmlGebt*u_kvZ12d&vLg>t~zTU-M%phUuO03W00+NQRmO`v5hVXa%Q?-8DIPS zJ*c1K$7k#*D9~%SUC7dg={+5Od<)u`DD4Sq z>|O+0(8VlKLC5CGKvwJn{<@eDlu)UfNpP0PBuXmyz1l5OZ-@C~rE{hD>BWx2Ngi4x zmVoME#QtXAcFj@*y^`5fg4-Xi*{UEnMdd%XUUk#`&k+Zlc8HH+c5nMR>c3y~GZ&|y zm>0o*_vDi@29JmIm7(}p9r?<~5A4w{rf^Jk-AHpk%^8Ks@dO#|L*2ame1Ye~6!#?M zG9)KV=Wfq$43g5Ql{~b|Xq673=XK@i@+9HhwS3hV&e|;U5-zS@_w92N1&8B{58uM) z@1$Ug=0MD>%Rfp=4lUC?!f(-TN^o(+zBM%Z_CoPmG-IY>`w7eIu+ak)>!!sH3J5dB z+T^H;1=6A=>gPc!fSovs)9QR)1d`XK(=~Z@JtLjtIdIhVA(z$B^b};9IyP}H9#$?PO$152#+b7NSR@39w&J%9}{hw3D zg}9X&`2!ASXF0E@mmY;x1TQg9NfHacb>es>^rP%?g!^zPGFq0dtc^&xFURfv%_7xY zP8s``*9fG7qmOgmH>^Jz$yIa5OwUtF)u~vI{@us@J*NvB8QZsatB`DI0getQXvKz*KXaTpYa9hUcgfG{6*@ zFxdT=FhRha@_LCm>@bE@a4XMC$OO{&> zIhK4-+;|$!A-ScYR#KZ&L#9zizh5|U8(2!o5abeFwKWIp6Rpw~)PiV-Mv1m@=(hgb z68_`27oYG1kS%)$#{_<~9t3y?v;qT3Dk=Z)d%XUZDS&+5cn?+A`zY)losmo-z&{1a z^_2k`h^~U$*H1=Z8%P*FTkj_)vA9WIBuS%CXIkg&E7=UKFlz) zQB+jaKrz#khn>u#JR=R;DtS`AJYW3zpf&Dg2&NRZ53$aM=D2>nkw#Z+_nij>-l|F{ zf%SI;D+0EIR(O5FEjD1*8$N9fzC@Kd0XZb15=4dsq=Q~(3=7X>ur(+2wg@RHOWl|) zua=ZjB7#SXf-Xk~0-pXcp^pWfk^7dZIRNM3UASjmCJe?ABshz-nn=#1PWR7Rd1FX@ zqZF7TS7}1`Ed&>V?!%Z4QA?NiulXg9})5)hbLs@ZV)_Afb7SZkD?!L7A`~K{hwzOHw*kpxmy_>+m(RES?FDs5{XS z6=viWF#o=|^QV0-NxNm%BZ#a~$f8tI7w_%@Q>odGksv0e;zsC1e+f^4Q?wFXF#5L& zNbEBTXiHB9BBC#Ghfe%&lTd*$OkJZHa|ph+VCtADro;3g;b}B+%+?rxB<9u0tkbpF z;slk`K&MT(wzZRWO@w(jL|$UnZ->k(8L!;{NF1T@I^AK=@b@$^&{rob_S0#8-`M3( zP1o*QSEoaDCrQF1SQy&_crn*9h1I$&;3PNBd!pgZr(QQRG1TkI#DyQHck~N-r_LN7 zah>@Dr$9p_Mr<nblPBTA~reBf_l^v^NpIYIp06wPzANT$D=k z0U_CLC{@bzMyBQGmo!vAujGTy<gRTR!)n8?2Gac58;HN{3^m8KeN)4Z`exo7XN-j(1SY$Dn#r5as0_nMnNO9F4B!Fi1bW&7Zr4Ga1!+h9f-cIA~I43xE{=T zqx1BfdW>K8mPP!_PEPV4SZrY>u)A>5#ihXlucpSbnjn%#nn2DeEypEswMeKmNxH564HyPlMr z(s-9j%9$6r@3e9@e~d>1SFe9PR2W0D)On5HWU)P1 z!$6r zc5Wn0bCzIDw3MaYvT4qD6NslaHiY7PJu6I!lOQ*LJFBUcfbzdh4qAIz-e?iDTgGN^ z>xRnpw$;_VwJ(zMk>Vh}R`d+@?r=@~g?Q4p8}xPtI~F@J#H4p(9aY3zy_MKU>z}4S zrw}Ond`6haE}q%rtF?*iYhNY*C_pkqxEmH;z*a3Ba$-c&h=-`hAAT(NyF{$8 zykJRaRCzqjoXQtp-&4UWf1pk`mYY}MxpE@i2&H6+8JX`X%IF8-RX!(fMt_-6bnQ;v zAKl8*Ihf^ zy0>}kV2@8l231p(PqfScqh%Y{IT%1E@p@NleR!I747fN&3XyIka2nEDz5Jk6;U3qO z<@z09Xb1rg>WHs_DbbUIo@fU!67aW~4RE?p!BNqsRRNB}0Y?-9>18cfEkS^rystw^ zS!Yhbhy4r0tO+ABctL7e?Mo%wu*VE`!SSbA=e!W3p`rqj(O~~tRE}$)%b0)ok#HDF z1QIb{|6DIu>|x6no48Q1evZX2UNA!>NaP6jVP~pc=N+-Aa~lpTaGS`*s|c_8#Nf{Xqh9eu{J|?%Y;M z(-qQHz+M;$wADQNW8^7owEOO;M)Xb$VS&{~Y#!{WsRL+T0w)gOhIB?Rzsz6gvthb6 zRd@*^X6%aW>4R4si1D2PWyZ3mxH=w}$)NBdQG_c2eI17d+OuLS$+EZ%XiY|!R1_)+ zJZWN$R1v9Gv#|bmeMxsx4tUgg-UxG>_Ll~vhSPQ#$>J{-iRknixh#q-T5=r5+!ffl zjT0>E%7mUs$UhRk-P80C!ned65!vdu(IF`(-eUaua3azJfse4@(q{uGAehicke%T! zzr%8q-tmkRZ`Yb-RcQHzYm4BZFr3$B&Z=ThQ~0rArCB;;Jv1S35PCY#r7wK2eb@l^ zJYAv#K<9mCG}@)1?V;_9*UwiIjgcSaMcx4FyHbEkt~vFdHcCX1ROA^Nqv#w zRSJSG+m#6zeeATp&`?PYQ@zz}9lOij%$^djU200e;99D_73C@bIL;^-FUP{Wy@d}a zxb@DnEVCSaBz-!14R`FJrpcDPCeAHa9DKqvBLrCgn7S|m0|NgTR;5+X?&cA?{27@= z?oY6#3_W@>7c!e}s&AfMP%>VmOSK{M{lq1^%F4ytY^+Cp8*$Fp>|V%Wh3(IUzM(t& zCaWi90ju9m6aDc#N9of$ml;59(!BQm0z$ljrqEp(5JXjOdSVMCQk8@e^KCXBd&%kB zqwuD=0f9}cdno2GfGkaL(Hb0V=6Tu>3zd#42^>GHM<>)$-`((-JEocdh)zhsd5i%y zxET8FLLyi+UhD8CPcf{3QzYp8Bt>Fod6&?XJsr-juP~61Ky7 zj{{4#&@P&Q$70@sy-xNLnnZGXW(T_>VARr)`O^WMJ-<6ExQMftYTml1wwvxCNbRf%(AaVac*?P5$wue*VI%DgKv^DC+Q{?73J|% zTD#auK*&)N5vO{mgxWvt+!U-mzh^e+Fa{PYs2sjoq;mdbf~SXVhGKcD)|R0wNgkT-fc!f~Sm7ka_TRlRI7~d+g5R zlctk%7^F4ybeUQ24?Vio;VLsKuYKc%ioTL(VBqI1s^{EwpC3O(eYlGJWD31?-qfGM z)B7H~2Srwn%v+H$hgEBz6*>YoSX2b$q`TwAx@1%Prr5dzpk)iAZ;#FAbVO!6HAOdY z=$QYnfTA2XUikP0%p$>6=JN!T0eA96H~h4YKnkzeIahniq zkF#A)x;)E3yn`^=du7noZ{r;>iS3b|+7fb1Cl7*uoMOsX(WSHNY)&?pMZ$2*KYshX zo!@xyM@u9zll=i`_NXcB$9@9I&|-w=d860!j-idRr&Au{Mg`?Q#%yp1R-(`{RV_Em z9h=!Qabgx`)e%hoQ=qfv6)g>#p-J*=mZ!STY{8Q4(0NPTy2m$hf!!UCu(gOS^=^{= z*Gdz%!by-xkkfmct@;}%cFA%VE~D()Py+tzv`nfmZS^j z&mW->#cbExSVZiSkPWS;XGVzhLfx&=Vxq<0mSob zhe&F}@foq_Yv$%v9_K;f8$Qp!B=QKttT}bwa|{HZ*C&EV_avgg=Z9K23JPT+A}(>L z{PU%Jwnvr!3--_e%q+*Z61RS#USSM4d2NQ%Aw&x`(p3rhp$Fz^Q#}#Ujg`VvgXkCs zA#|u4QkOL$Qx9uo$dAP`93MH-a_hSl5JZp!@dYd|&O}SQ`_ms{~Rb`U7zXZhV&{!4l zO~=SDo`H)#=kP;J7-RNdAJs9NLw z;pK8n-fXD+8OSB;JCJvVINBTebP+uVCVE%7^$#E3Hl7nI?!Qyasq7E!`?R2R3UsR3 zW(0=EJqePpj*KG(i@!8A6O-O;f{ay5h;4@KhWzA2x>MboI||^NSTU@bVb%LFWrMa# z=5t9C-u;&Orp`h5DLNt`mDYIiE#MX7Ou~y!-pjDCg z4A0$5y=FN}D$MIq?`>SMn_K%MN)_EP%+o-OigD`Lp^Hse;Qvo+F5F{CfNqTOBmJ+h zdUGk#3j_#GzjdtawJR+59OexCY0J!i!1S{6B&pTn`nO&vg}|bm-yD*!R}EP%2Wefu zCTgcVp0jjylD2-;;Vp$f0R+Anr$qRnu!hWCq_u?fWF6#nS4sW8>ksexM}@a5Eg?`Q z%51rv`?0JqJ+oxwE)k;!@juIv~u4NK_@*InAe|jJ406>8#NI|dpk6y^WkYsui9hC zNFO~^Vd+m5g!|IK@w^xP$dp)kzl}tZn(Z7h4~k$?0PS6uWLTW&yn8|8c2!B0OT$ts zgX`&**KzNMV$-ptG3VCcl5$lL{}@N(z6y9p-rzn`bZ^GNc{DVlQW)FrdnPZ2jdhG0 zdBZ8xw_5XIXE_FR?4%k3H_BnlC_eAM#S2C&=%SC7(v`f5n-{L@{@`MZ$50r(1bhGt z=5lKL(H|yL#{$Jqf3rs%0s$Qy7WdmFk%t=K{ameP%e8SlLOWh#5Gxl%#ns&{@H{aA zBgq_!*y7`IH)H#e4tZ|hcPunjQbibud_{AeE4^kd4JoIR9k^(8$g_03EuH2e9G$+A z=%ZWP2#!4m%I(O=J=ee}mEKPIgl_6YCs^`uNBFVAa4QvRQ?Pw05*{2g8R7P=E;X z^NfN-HW0iUSP0eb`(bX>eNhbJ#-XbiRvNfB&nv z?nLV~uf2lArtz;NWH*y|It9{!O`N`!fZ!?+OlREZ|508evZb_cFd;EIN?xIWl;{Eb1S<%+@$bplH z=o1zdDLV=AvP|0?RWk7`dyX;nF%E?)Eo_Z@{bC4P>*El>*uf?26?NnEkh%W_dqc3b z#L0b(&Cr}!Fb zc_AI*8e((=j~CQm5Y6R${u9hhwr`Cmz37*WkUtH2{oppsax5x}G+IKI^d)-zshO2p zyQ%=`yO+qVqQt`(#d6x?*RTuWK|_LON~;_!oM$GfgL&6rt~q(T&s z4?Gjkp>3f)rq0Cd*5Azx#NP_5mxsUn?K31{0nAM@o>5s`0v6N+Swu64LiUx)NO?8g zu=&pd!kelF8nm8H#rDyPUP`LS5nqH7=@fAv)4!Jg3a6@OBMRN_-1sjqBoRG!jbFNv@brbOIV~6$pC_OWdS8EZ_<7+Bd-Ah8XJH zkdSn?I!Vz_#w8qk440J~EK|y5Tcqy4*PEx<<17Dz`5bF}Pt?;RaamUMg!7&#ho+M0 zSiN>CZam;u`5S8UL2T_pDQ_xy5e)#4uAOl@L=Xn3ccmq6JpxODy07=U0`&%*DFl#; z2O&#Q4n~LL(Wy=oE?V*gxt=hP^8jKf68lV6=4v;Oe|U${E(zYFT3AuVHS=Bl+{jP4 zDQ%+D>1NjanbP_KLd&^fs6y9HqQ?3{{K6RapkxukprRt_h@MQd;cW4{cyTRJh$Flc(Tq+EIS43%l08w{_sJuQe}{R|*=;(FDJ~0bzVP!6+R`xzao^ zNF!`pU6(*M?@UdbvK8`_-A=;o$lnW-7n;^L{B<3wPCE0zaCy`v^XqUL^`XO)ZdK22 zb4A;xTngGfT_5?60?48o?Kw>8;BzUgV4<)vwoI0#PQtu~pt7=Z641160aIV;&(zxqUr+z1X>CaaE}83{0n@$9oCxnn z^nsQ4J0%~hv>PIF!O_0@bhYPk7!)!zhIM5Pt=9n?|5;rF;sZQfYm?cWQ!HyLBNHXK zxW64lXQ<_DCVwPH$8W`?G8=POXSTlxH0p}W*;I2$EqxKcjpJ((LiIiMDapLN3Ca#W7@|NlOo~ZLze^mKn)tsru&~0{|4jV6wuBDX)YyQ%|a9)7D z?muJ8Sdt<2=flA>sTJC~ArN_MkoB+(68`QLPme?MBIjP?#EbGg z&_q5`!}ya5CO)UCGVMPjl)2ua{EA^_2 z4NNJ0QYtzU6({?Skvnl|jLCY?p)C2M$Of!iqgWO?;z9{(Td%y?0 z2X$AZxG&!W51?k}ciMNUwLg5=TLg{RDsSe?VSfCwe*}mg?{wlTkqZ1F0p~p5Gg%qF zQ|vL6y0F_9Si5AwW4u#H16Q6Yj;5h@LXt8g9u3~=>%MBPId<4?wg$0hlv>-Won*uw zjh=~oAA3csi>oBKrI^#Ql1;iO^_cbdp)n#!Z2jDS?11`mX#59G{S$yJNG*WRzvi-i zcQzjK8^ZYYH9}+LSV5Z^f)11Jiq`&`ZK%?Tl6~avO>aDh?z8D@NK0|(H_UR zC=RFZsw7Wlge3kXk8m@MI#D} z;uKQ-hrwYJ5T1g#$0zfveQKd64QkFubwBo=sLt(;t|38Q$Mf3mc^FJ2{D}=cqGzrU zk~WaKMFr0|L26&E%FZPqcVG-iLDMYb-Z0@WYGto9)QqDbwBO4&P73iloBxzZuWP~9 zUu&7XZdI026|=1jDc`cw>vKSfS$?!#8ubxSZJ;VcuvSH~&9YUm86{_bS{*N#Bu{MM z4#+l5cdjIvu>Sr~5QD4$=H91m7f9ALVD0*@#ABtPu&HacS$hyl6kNs3k#)yY6)}&mZBF&E6tZa)fqau0?0mp9x!`1{mfgxgTWc zDt-8wZKirPw%p_T(EcAqB;1nHuhE6t+$+8=_CJ?8PcqwmTjM|iokJ^j zfoa3vLS!ckzPP{hH!$`Wb94K20q>H-R7*Zg?yXXeVKFXev%EHwIta;sZf1bz;jr)n z_g|+;%Tuy%0-;OxYEY;9YsHWd;ght4a{V41JzX0qKu>X>Zf}C}8nOOnR@QW4&+grz ze2=v=)2tg53yYaGOoJ8CydF1dw9g+Tj<-428-+kKqObdBFD#&Cu!g1qA?H(j{USF~K*!sclV|k~*FzLU@Zc z;pN?gsWDf6NgkzxqW9b}R~>*ntsUP!hnXyzzql3MpZJ}p zvo6Ac!Xa|g8lZo)e-XVlo-SE9IAun4|CwQgW@TdUk^tco!fTUHU~rN08j<>OWDU`l z^;z+(R+Qc|s26Jw+n@E8(D3gFrk`}@2ZlNSOiQ|+28>2pxH}jx@Ab17y}`QLm!A@j z=q@2^Hudo_CFS6uL`0MgiY;0aq|d)R4Gv7pJ9#xjo~B3m?F1pX&+5G6Vm5H&zOw5# zYjGc`Ks7Dr^eD&fF=;zSa~AT7``J}y;Jezaaa!IkZ2I@QTms}B0)?G~g_0P4>y$4_ zWP0F8j&Aw<7x40MYM#P+BiotTis89`7B?(98PbsPC#s9esJ<5>aZMI z&a$F_dTz-3PNqz-+4wwVL^M^jn0q|#?owz(e^dJ+|7euMc3=nC(v@{zlRN!QcH^L55-?tI+^{Uh93h$-0dP1rx<5&yu&xq?gBg#T+5 zl**HR<1d>|9kGu#rCFa8X}l8eV30sNte%{5{$}oUsFhHdTDtaJHAfX^?vZS*Auh$~ z;ch2hpeKQ$n})(uI@J_HZgS18+^5IobC_ntUV{OY{PtFt0{eOZDya7)HVRUj%z%^ne1$$L5j*7Uqs) ze-qEtb1rXwHo)kdI*?HyeyO5U7CfwRpDcB_XQ^gF*3AmEDZplGjnKAxl9Zg>76l}E z=A*w;v(P~aw z)g?^EtldqO!X-g4gF-dTyT{Zu;ms*k)S39~(0-+;2ZzO~>zXTREB;ZDh*_1p2$ zy7I+|+M4A#v=oqy*vP^aBM-CCv*mLFO{Y9V61iPn7a5>}Vjcbl*U;3|e46$CQxV$i zvE%>3vKJdA@+D9;IA>S)8(cOK?PO7H^*K46aP3tI>6o#xaR3&hc@s*>UT%Nr%E!Z3 z^FrAgkbtA>=jX^%mS`6BaBaXF_9)M36WUlo#bGYPFEO=YL{V=0=*6VM@S&u2GMY2H zw8Ae8w4~M#6`t|%_=Daw+d=OncN9FOvJE+(rr1??;AgTTP@SCh8|&v6^$-uI<2#sHcD5;Fs z5Uyf&Nu}~!r?d3cBB2xFbpEz>|EHWoWQgamnzCsBwEl)t6bqo35PtYsfm(EK0I=e) zRS|c#hZ5Dj*sheMx7Z?HAqlwDA5uc}Ac8gL} z@%cW%Myz@2zt4s)K9(%tQ&G7ad-<940h|1{KyaJAd#+{vz9D9l>a1twW=U~z8K9G> z&-e`8s@Cy8oF5)Vz-f7KqZv}Jv!D#9Sx8xy#RrKVFQ|h`jK8ZZG%AV6^1>v?W$Ik2 zD2hg)x)Ocie~R*de_Q35w0u11)igFf8bEeSN44hV<9cUPwb;?Ts@GE&sQ*3 zvan(XfWkS3-(2GBlK9wo9p61DibVTx3vp_^NO`eYaC(=4X)MoB-O1#egXiIPD(Y^5 zoqo2;mh_@To_YBr`2)E};oz6~0)=eTvYRTYi}s^x>eCl)O zbogMjY~KMySsNu&CJr!2+FV#*U3UMJD*;hwDL|aWp!V=3^w3G~U5p!CxFqN#Q@{?P zOj2=bl%}-q6r!QqoN}mor|#n@wYFlGxkvkHefT zd)eon$N=Dk%?$OWdx7K&G4GhnOs#>9Xa1%oD)nc|J!ob9`z7{y{I*3AtV?dkV?d#wZ)fGn!>(Bq~ z?FsX^XaLS>$|QbW7h;ua*Z=jv?0&CLY@068PsCA#+XgwPs~xSC5TUMbx@6?FqJ7U? z*K{Rqf}Mw#jU;x1d&X(9@xXu37Ug~XTXlT#ED`s-cbje*lV4T=uN}#PB7P>Cy2Lc5 zu}lx_0n^;`eeS0kWtTY6`L77K(!hE8L#!=vScK@!rwWfN|L7)jWszJW2aA* zxJP0 zrnEA&59 zBuW|A98bS6vYX~A>w;7?u|~Sdpe{e*xAEj#jDAb(L?pFT zx~`(X_EfcgQ#^I5;@`$g2hK8B{pC2DF$l+4gbCPJ_HoDu(Mk#&2C- z*5AMOvpld`^cT}F&7fuOUEwdsBwufpF||YtbVF(Q5-qz2;zUf>`bBafc=h+M##r78 zBKK9ihj|d(a$zGlf3&L3Ydxh_zYWwKpT*<-_NxBRwBZW=Tl{aeB3ee`@A4Vk6gqsGkZd%f zuWxLhU-@)JmoR|-=>+aS2qGgu`r}lp%bYnJd!p$tkow-t%50yEYW}8EZWH>zd0g9c zy6#~9)}U9z@w@3#^O%eA+hni7nLXy<08Z7pU5nXHKZTwjKd>~n<`{*#oLz&N>``I{ z4x6Yp1`cBGQt8zd|1<~xbbYY;0>dy7 zJvt+{%7@wp<&~HBaGW8!tkyqfJGUJ)m)RX0Tm;P@KMrI~c4@h?P}|fr@NOW< z?FCDA)_r;lJr9^*^YB$q(0QH*nz1eqPP@!UaT+wR=-oaDFYyu<0zpM)0dj^aq8@N! zN)|DeS@q63_p+He97wo(~+AfkAPy@P5O`1Z@<>@?znlPz0(}4Ws z4Q3l7ggN0WE-xsq>nU+`PcU4s9P)H%3m>2QZUIDJ!GIXV6YZBu)^?-SH&H?TXwlI{0?#a)ICgVW;UIR~%TvcnV`hv0{c`#P`t*fqO-QeE^0P?*tLQ`V zS6t9Y@6G?bUVnNc0jyuUwI*J%)}Lw`DYR>*HBpOlm3@B4?t3@$G$7*eEgdec_?n|Q zD~wMi=)z0M`r5T@z+8L0|_6wLyocn*)kD&Va6 z6VqoZa_==vZpczODbC! z4=Y2fGEY>TvaQLbK{Lmb6K0SWE?Ra(ivUb5NS-vq`pYoa4UBTg&dXT?=* z_w@9%nqM!{ZuLV`ZC1StzLjrdVL_-nRG~6s%&PH)V=Aw>7zX4KD@ljE_7f$UxJK|q z?yFE~%j?Wre~U`|+wLRd zbFO`YNhAyuz@?n|8rk$kDQ8Smdv5c>hl<`?V5W+ZzLJq?xL%|GWT19SR|)YrHy__~ zI6Y#=Mj$;XYVZp<68<8ur9~7I$9lTO#|xifgu2~i?IcZ0E85FZ*HOZ!8lyrrWp_+l z0Q-JX?EYubDKEF%=8^ve&}dr+$r{(i94RX@X?(Pbm80Y(kWcI_fcE0V$yqDd^ZG|i zO!v@Y+L#-9kw&Y|c$(~IU-=EeC4&lf(NlU(^L)l7!7aLcU#;)!CH5r)q0|+kDGFhO zGAP~*^3RjF8B&v@bSiA8YpI{0>2vJMc18_mwiH1NLv?)n_kI8pKhDdat9>b@d3g`_ z<90e7)*ephWw^n=0k!4Y2#E6sk#0QWV~vI6x!;zqb`;3J~x z!~~CU>K`03+eW&G^g#u1OqYq2#ooOSV1Km>gQmF%Ttb!I;21@QZ$c}A&<&U951!Hn zeMfI*48a4lclfbzHuEddFpftHu?^R%hi}3*A25yDG)aa77B%s9nLdPL#D=4$7K2V;|Qch(CQ<}}Dzed(pgpU@pG zGXquG)xJ88UeC;NdQLzbhn)22WH?E0@=TkrFeB);$QEHw>_bKDmmig0yrKf6bioGP75 z9mOUajFQRYcJ*Q<-!navHH*3teHi{$vLy0y&?@AUnFLi$p)uQ*ZU)mU$;KgRa;mK* zo`2BEerrp1B9g`L<5q$^^V0T-ftu0TBT1;D$LVSQ%EDOreteHfslmZ*J$=2>`YZth zbb3oj8Jb$k&knBk1flPyp&+B}@LbtU5&W)S!eColIrg$bqW^d|=5-!(O~zP4p8-lsF@t$+UsI;FnNyxS4; zpD6^A2nwXLd)o0k5e+j$?HW~d(&;dsl8Js6u{E}-bu>SV5$xJTAPc(MJH0B7Oef|s z>4=~}*BW9rDp=1MOuI3pe^(s(iW9Cl>0*^ZM{^u!Q&?k-w;}pnj;h%nedyj6=)j70 zw`jzD^KUNo53BNzT$dEtEeIXkR#dRuR9ES>4@}{5v&g!*wDe9^5b7iQnYtld!F6i= zCU-}C*2mKQyCt#4$Ay7SBUPKy&xR4uI77E7zPOd`h6`@D8HMWH)11==&{rB6BvH)S z8gFy|!{8tT&46fR>#5~tLaje6fCs9RT5xfCH@rG1BQy&0F2#7mFFCmZ)yya!{bO)O zGH;=WfEJeY#4!srKM}$t8baalsz}Y?=q2u<;T%H=P4>KqB7P>A$_?7?3!-z+FgvlA z&{dV;?#xwar2TZ$GA?3o5n2I*W}m5>YvwY$pMYkb7MwmC3dlAq%*(YU`VM-J#tE8CM~w;aL5)ky2m|{lEhr zp9>iE%K|07z}c%$L>5|KIt8byc93)_T`~%^(EeKwk9Eo9)WLb92Km0|DfZE_E>oXg zs9*^0MittJtjN|tY%z+@r%YT~SxsMyY>IGv9vj&V5*vd4^0>d$1(hH5|F@i0njIv? zs+XHOPmtoRP!{ihB~Xy9uGeoXaCTf$8Piyc<3hl=JklR}19_5D@zW!kkshN>cV*_E z9xn}RjcY|twpa$H){E>dZD;$Y69pQoHY%fv@zP3}v^mKtjIB8njyYcH(rMtLzQYht z+|Q}p98um=8gLBTT$0Iu=}aqa`_IZbfBTBmhwTQm z<+z9l>y=Q6q0Q^>L}_E55v@h`HpfqhZ3Px-1*ePQMfo~P@7}%=n zgErNx^W%+og^dF2s_WWW)amjfUFx4{T4YuNBn%`7u4zxA;b&=f~ZFbQqoh*MWH?0s~ z#V3DEish0!mb44e@M^HDArXxsyTZTumT*&^g!#K^g^Pb64gWMcFG}vN`&189Nc+S8i?{gW z4!`y9(K1-?dvXE7|Lj`!*L(Vlx4ZqBJ^W$Q{zG|bkOE)sOv3+9#s2GmfJ)uX|Iqxu zUVwR90Bm6znf{>v`!Z^keK4>S?DD_;^IxC##oxMr{;!vTirn7+d7m4o5=y`@bw=~h z{9j*p13*9C82`t5fB^41fr}&}QnCJ)Q2xi#{s(^j-(=7hg~b1ymY@IG661e)0#x+* z|G4|#rS={aCw@fHS^j}!|NU=&c^G`AGu*!l_dj>T|G!&FB06JGh-rgGF9ibn!yjwu>ROTU zy{;Z_SyVjfG>vpypPv|fLBL(>?)RS^alwO5Xkt42fR-KRO(I*41gJ_c`1zftP%9an zrJqj2;JDRmeZp;^RfpTj+Vt-IyFy7|zMEp|lh4qa*HhJ#Cn=}i({JYC+dkJO z>jWGk{9<-pD&#uR9!z1L(R>vev!embNh3!yLdxFRlm6`1(BB&-ZR0wQQax zlKhNkS^wUFOG1NtB*JWUl_=_$#P0WNFVDktHaF2*IPQ3@C((}s2GzShme4B)2n4~QO3a!$1Rom!`?YD+aE08<{! zx13_+Jg=S=xu(2?_O(_`B|o^_ml3t#pTCkJA@Aq?5vWpBUJiLPkI-sd%$uKbYq|6R z$1L)E_Kgt&)jl+S!zS%ty>==GyytceosA3jwg*^^JWEDLM<;neOZSFOP?>iLfFC4? zU2cT>v5zRG@bSipxozYlyZl+<>=~M^8JybB>JP6 z2}rZvsIK6c-1bApO)?8Pgv!<6BpwUYkALQ{L;v+jZ<0yu_|??|edoPjU)QeQ%#&`4 z9e&1}grCmZBS-iuh#qvIM)hzs_a?Zrk%!mA)UUM)lyM#ui(Ysy01n+_7cJxe1~T9W zW>M|m|D!)epv1zvV9E!#IXyD>P%5UUrnWPb#GnamJ_TPoL8>~*swB*SRxG;Kmya>D z;6n+0+5Yw2Nr%|HHDiW%O!J!I+GJu`?~nMi6_?i9Nbl{KM~Kun{ngpV&KgW$2+g&v z@gek{LW1xMz%c?DTs6kMS;8{cm?ssEOAi~@zwI#6-{WlIOvPm8J?Z7ouwd0HsfPA@ z+q`PoaJ%MQ7P}fK-q~2`**P-qHAHs_D_?Ud7pjTF0;xp+m-s@AvQPJQE4@#QD89HpdDg9ETy9WM%BtIH-M9Hw z2W;oRIhzX!P=*zQ-T$LP6ZmYQ+^r9Shu9LE?uif@qts7mL@k66@&XMQyWMV8McH^z8gtU)@U#egm_v7o_3S4Q4!%lfsi zYgPt?)pd%;nlDAGuy=@%$|SX*?AP+6U+|Lcvi;Og54i#eTh{tZ;|^Cnn&d5##45P^J+Kvlse1ySWwnXUfu1hqKwQ8VuA|NEx{9?IPAh z>efJ4ii5^+^nN!>i^(hxwFV8W1FQXzE7y~{=MTj6Y*U+uUsYd@r{%stcvrsJ`Z>P) zJ;uBze3HntBP?oDvx&jAL}$3zYh1ewvkAq( z1|RUKy4inqTgtJU6+B!?Oruv{W#hK!y)OVL#(KjWnO6lTyOR*N)(YiFgg~yJ1-(+L zPJf0N=iXXRx$mwBiO9`wTv8zfKQIkGd9<`}(pwWX4@6#{TL>846~BRkhX1dR00=R) z_(AR4nMe>~QtvCFiC$M0vl$;$5wYq?CU7KkW=rG7c0^`$ez}{y5;~Rf)Septv@oX9 zb=2wna<6rz{+rWtP9M1ON$2?Z_~sTxjjrob9(!2d7fYpUBaSpNI$J$R92g8#)!n3m zx6!-Y5H~zfJSOW}bN95I<<&5viqE1#+t;OJx~Z$zW~4foT%ByxS0_7}T&Pl6ujVxe z>2QARYDR4=90ON&>HVGMM5v7U;??BgK`*F+Ng12z)6xaT3e_1+KM+1t znDr(w z1z|hm10kC~!?y^;jD%S^m1gRkb}n3M*wU}hwVvYsDX!mw3xE*0!+E*pDxccP{1)Yc zCOYhKq||Z;Sdw_mAMwSDoNjp(hZ%`>aS=1=bsuQx)$CBiFZ@`$)ahey21h(GWG&_U zh09hyR@&3B8egiR2L=Y{KjLc>R_&zIZ(5l{MkC_a@467h(Q#`%*^$?+;!y ze9T-_t}Lm<&l15!eWhMx?N~LtX)~%~H^oX+C9sKEUcdHC)3UWo<5kd_Y_^1Obw3mp z<+eF_U1(5WnWK^RFXW4en9YWM<;GAy_0CRjv_8?H zs9ALV{Oq0C{`Gs=M2vh zG^VdBG9R7-MEeCQ8WSoM>mpg`ItsgiSDxDc2C9jEru+)aPqt86>w2M%xRD-(H0h`< zYi7odI63r397at!7KZSxPwZW>3_ShiMvWFLTX1H?*^%=@9L@!N3@*R$cOuqcjA3n=1ECj%HUG95{E*8y$Bz>)9L~*A&3->zub|Bv z`_`}a*2)i;fqSo(p*o>N9Bk&@3Gq$!NA9kc8x*KdF}*KaujLS^=PUD;1Gz`qX@N|A z=0Nn+uTT(qB-M8r{<1@mqIs*k(3|vzK){KpD~`+b5#;$B%sS(R11O`@+KFx9vSAlI zUY!n=hT7qx*NQm@)t(-y?Y8KcDov%-r*!|`&cEMQouc@jh-!ldvo6H0bf(Txq91-y zJiVDmi}vi9_})`4pDAa#blTi7GJX#F2JsZkIz{S2ZGsJGRiZcSpo802IKvXsu67r7 zQODn~UjjR$=@e!rDoSa~os6669CPKCE`CB@fEnU! zryBZ+9M?2BB%JXd6FKx<&fPK-PXAv2AYGD;)u^T?2Qsm%X1Q#qD=n zY^QNUDA|+O2dWU~+gq2zn3Shm(=vQ^Q#roA+KG5J-b{eW9OpvD7Zejcq=<&UPY<*& z9cDL3@>^}hP${M%(Y;CB=UsSArdL5-PA7@}tyYtt4Q*-`EkuU!y0Rrh*xdH!Vvp81 zWit>rSI)YiD4JVwR6<&8q=V)8afr;ggTdt=A4!}pPbdVumkXTtCbjM6>z6bTw#t?nh;;*V-j*@B5wG1M<5yl`fDf@ z)pkP~7pGgchW$wpe}iV%Z-Q>ZA1LVFml(82I<0hDT?RKSNx97A9+7nK)>iMG<(ACX z{a7j5sa6eCHe24OK^Q@kBB{g@^ot^T5)o0c`AS_MhL&a#lxP!ANPchAIX!Um*|s~E-6HuM$ajWZrs8`Q6|!9b<8Fg@su3S zQ)G-Ub{F!|I({As29S1T2S=K9fU{-}i?mkr2%gD`Hvl?#Crb0e+t>1W21`b*9i5`L z_cCxr3G&a6PraK}T{lMx&EX9$!;Q+u%vvQuu#NE!mTyQSzRv=jgppJT_isI=c{sRO z~pG4Cd-G+)x2^xMzWC{`63v|lFa z%}~}tR7;}sXFVAu)b{x%=5yhEcp$@T);+RusJMqv8lQ)Kmk~>);u_glY}tJiPBc0K zi7%21cJ0VXbvx!S5|xXgw|AZ|o{Qu15!8ivN=5A%XGw;bwa@5QdjA+0E>gS{hr?}z z-h0}s=gXU4u^2TKJhmT`yf~(!C(M6-TK>f2_XAKq%(Rr$DBNZ*9xsWPBV?3MskIHK z^i|7`fNe}G-@?65edMR}VwpnCuRoDZ_1RdN@t#R`rV!=?`UC!hXn`t=o+;j~k#s`h zmFNp@DX@RcyJE~MO}+`9|LWlk|GL3r7By9~g`fEa>bM+$I$xO4BiNVDBlF;KfxS14 z?0~~q#$NtFy8KX#~^cV=S;P26xg!e^q6foTzZg{DK3jdQ3qhtLcSnuK^!=vP9)?kDe1c1N zWboPlVtlfwr#jcxkkC3W(=c3 zUQ0NT8*I-Wolz|pOoYhpP*D)0IQ}vRr9ivFUZ?)C*J>R1S>pWz5>T|}oQBxdyd@Om zk$4YtI@_md<&O>Y55@vlEz1#_Z%-UZ4}b#iEAN2r>Bh>tse?)Hut}m75`Fp0j>wRE zQ4lr#`07sF$Z`*LYAa;*H)ia<``A_?qKI95wdER#tfUY%1QACnoPlT ztwpqqtLnVfAhn)tYLkL~vw_uC!<~@83w<6&qaqhbBD~JPdaR6b9+#5EZTHGnGl@qA zH|dFQwaYFR21qp=D=)Uni`B|r5_f%KPh9RdmPAI#q?OBYL#97_TCWi{E*`ty)BI+o zxBYOZYWJABpk^^;za4S#k61&@$ge9^=n zbM`4RMsT?Ift)!ep355cgjqH3rGKk@VmX_iLH*14ag3F2Z|Wp&^Xfwwfyxh2CeD=F z*F4=3lwzvbL>!AShs$idVg()XD()LM`1Ir?pK5HzF_(J5p6*f3N^vrn<+WdvJyg`M zjwj3?==_}Bk+a3$oZ~us>u_e#C$h{Y({)wJQ4jTF5}uMpu~={%{d1X*vE7IfQOi0i;Yx?2A=?4~o$iSk66CZMfh z<4->*ra%ffiG-u>mFhi6%Me`gs&|Dql;}40yX&mh(`8vM8jAbnL35QY`zS}NjMnCmz7#WlEW-52gjfHO_k+Nm1XePTmDTEp8j3!4u_hph>C!V0W^deHOg|B4xcDp3f@ zuW=aW=3kr+pMNA-E|^k&Ri=kqBvq_6oPEhpDg2-TL|Eyx;{IRN=ddmUe(xa}YKyRS zCA^JeiS2R4H1d|hJj*AKZ4U}FSnOsR*xAKwEmyY8lP^wpOwzUj<%wB!*7)t`;ucwS zekD8azt|PNyzLJTsLO_1+w*mgLPTTe45-hDv6l%tuB;OrN! z1s?)W*ydU_|Fh=n7E;UED{KtG!OWYu78i@bKUP0Oi_--%PfZ`GH`bQ+eZ%+hIryp# zi_x%ZHst!c8M4&s$^m zvTC(&M2+?&m*^N1CQ4j55;!LjjOT^$UpJ|{H<)5Z!l5P!Cf6|APFF`)u5{m5h!@C` z)bulE)~(FBI+-qym?WoLh(@ik9*;3<_UtZ*rlYrSX{t2sz!igo_UE5xw2aZ{kdELf zmLzV`jqyK%R!rsq;8WYtXEET4m*a|#OW$kk&M)mRTkdG(eQSRop0W(pVhDsm-+UdQgpSjMcnm*W>DC+F(xrl0_SN(o;zdvj z+0{hz**+NWp>Zm^vs+-V#2_En`AW>=&`fQ#@ofc>x`Rsrlq7AXBNwN175kny{ItCx z>37l|fPZ%o&m~_e!)}}BKoMd`AfRRzLe5)6uh}D(`#fGAAW;P^;l2~|7^XDCCJ&^j zF;U>9PHBR!>(t;O2%E?CuFwt`;xB|P`(iYAua6>cKY0A;gI-;jza{4OTw}+&Y?@vT z0~kd?AO-i1Am+(C@aX@<9Q*wPrNmSXWI*jS%m|7DB3wz7OXxl2Z3-}o5}A>XqcW@0 zLw@enMOVh3V`BiWu+KZSTH5Ja0l%X)cvF2iPJ?X+Daun#_e~gSu`GFT5{Z@tK89Q@ zLyi1;W~lE1mC97Eh3}^>M_mRU&K#r20v`c#XbGvh`8wn#7!}=qq>U#+C#}rMul^Mc zt5@O=tn68mc+p5`doq^(C{knlO>wnZ2`zdVylsd~s(v z*7KNN-QpNud&klB@Vngk)zOJsiMuLsjywKb$bCO1!UC=4c|>H};bo&XEf%$+WmH?1+VyBcJrONZuYst_}}W%Wv}7Arsd} zC&%?RZu-)nRWM0Xo~wkNg7kl+)yd5?S-0mE`Lxd45x-eK4H>AN^)bv>Bi20favqPQPNvBpWlsLfb6 zh{AJ9_44Prh}jLkpIxo?$7+Kpoz?MWN3@ppn=_Hi6Ty2+_faD6z8bDB>`mg;_c4No zxUI-%S+*uI+pPAg8SOw~TE(A&JT;bHgQo-;w$n+6bAQ|q1r|tl-#h&7tKVxyn-iy& z1I%4iicBVrFPY!fdN?*r;R+a(-EjzUkg%|~ri!w7yjg=myX~#(8F)4rf>Ecc1#Ocb zbK9RdR}~y2)4mfCUp%wD;?7l&YpOfR%_g_MIQwR;*66C(!6pZ?QK*LsqvO)#R*T=Y zZ|0A3qbdw0=k`>AN(?>GI*xReDp~7ME9x>H`8%6_v}+@mxi^*012IpU6#Y1_ z5At0aaif;|&(mG{f1^U;>jZ--FCnjo3M4#LMo&}~-XIWKF0EEt)+xokzOl1`T^%EV4G+S=8h zDne(i*8pZn^E%&&n4|S?6Cwdfa({>wQd7kj6wwKacs&eb=ZG#AzXkgy7!1$HU3;9X z$;C|6?ge6g` z3qvH}g!CGEoyz61@QzqSuE8JM-%1@TH)JLoFTtmvc5-WQXs1jab|L8<5GC5>%M%65 zrhbC%90i7ZSsbo^wzc@aZ}Lrp`l>$cfr#EMUm+E5@8s^GY?+*TY_ll*ePc>88n%JL}KN7R^Cso_7FZl z2bQKG_6mWwjy3n6yalQ0GwqWuiJR05-2Eu1%& z3)R({Gz8=l1haFxJ{b})Dzi!ihsUj1`(g5I@OoPgI2&zlw0)+lG+DB!`cSbv*lQKL za(K?;bn>01-g_|>wtF5PUCy85`W#|EefntqdIx{VubnS8QO|d?ZI0q_I%(-=d8`ax zH=~OUz=_A`vq?C!d0R!z4P_U%11QEfu-zytdTAcD#u86&dT{V*~Mioq44rcEbkRP~&~M z1F4i|Pn2+=C2IM+*wiB^Yr^4TdW(%ktB48UVF_rPJ^W~lU(Yeo;(pgwYWUOwc*q$&X!kV#0*zn*z&$UO=Np9Y{!>1hHBBH`0YQ!kwarm8uQp{I) zYohgLhYvUGc%IYnuw3WETB$*eG&T`CLvD|j+O_Ot@fe01?k%exBaw!WMzlSK%%&GG zCC!~FueOa;LZoR5806ev%kaDJH@3-u1}2`;!0f>xOx!@E%5%rV90*A`^u zehq9HIhLko{ptGmICUQ)(>4*aaXc{g4jwbr^d@haRpt51jtJY)qw;U>Cz1XZk8)XQIPrh!Gj;>+`$CzcHctm~a8&*45Rp-dls2a*-2N2>#gbk_Azt9{8` z8QNu?5w=1(k_W$hFZ2PlDCS{CycH0b?@vBy`_pV%W$hK*ss3mw88VAd4Bu1fG@OP ztdI4W_4B}VjgllD{f-EXdBt>=i$o>QrA@9KW*N(cr6Zf*wC^Hq*E{X_hNhEKLMbAZF9ql1fuOcCr+re)OL1 z9d{dRr&X%wJumDJOgE3x*nyZlgfCpXSbkXdDhKI8#^cv4qs8t$aeIuiL^b8XN*Bk0 z1V7BC`)7ZiPhTa_ha|dOX3MbcnVC2s!G|E40MFyiu2CBh-!}R|6@NxhaAwrluzH>B z)JR{rKz6xbE;DUR~{Q-ti|og>MxA{Oa70tyfc|v}W)03%Wp_f-F_fpyR0t**D|M`;%x2Vc=up+)aF zi3BPf-ip@|02se)&h8fIRgW2@7?8Txy!QBum?348km2p+Ww`fwNilHZl@Z2k)b+`R z3w0X9$-G-eYp&Vb=QyqUr|Zq!PR)t4f^H&Anw3HE$AczTOsqUnfdIB~Y|v}*7RW#H zS2|p-9l-b4CRoN=Pj5T4957sPK5yREP3xau&Pkj)o8c28GyQFJ9Xi33!S{!1TX54o7iH1gRPZG_w=5VH*rJrG>dm1H%DOyTvt06{t z-3Hk5Yqk@0T}?3ga!( zb#HEkD0Lm$7#YWAa!runLWcp&hy|#H1WiH`0+1QtXZ?k(a>1_iH;iVTnZ5zIIkKNo z)$(8%TBOY=CwYC-C2x*0$2xd>CB(`-sY2348g$Q<(}km%p_WMK?KN-8=OTqi-<$G` zS&MnK6bj&kz0O+fA}&i~wnrnL;Uy=!>vJnOx{oa1FZZQEyZP}DO`e(@26eB}?V-kg}*ND%c&OH1W& z`knFPucEE?r7_ZtSbRgEGs0fb$%NAX_?Y-OAy9tPd|sQm_vxd|M>K<=aIc%4@Ev9~ z^Q0|_kd)gZCRZU*t~8nV+xJhTk`u(8X{`C%GxZ8AnguUy6cp%qh}G`NH6zG(n58qMrZMD<(p~gkh^s6^A!!znC4eULI%eout8omcU~p2t3| zJkevSvfU$ucGjpbQVf75D0e6(+-Dz~O<-o1ull?L@KxWIZ^ADr>=fOs4kJT!IVJK* zPPy1As^ytxFJ<77P2-^$?{HQVPckO+ElD_yH2Twp*=+TB`8OTeQrQ(OCL0{X&3Y`1 zhXKU%P%&*$hi%ZbIVWMx{#PRx-7@SzLM_LECjYRtSVY`nz&*W__zI0VZmp$VDmWm^-fX)@2AT!-WR;Rd1mbq&tZt&9QRwS z5CVoc$<1lr2syDw*ClY02wi}n(iN)bCqyUmDOwBUQoh_`2y!-LR?mqdVvZ4tAHU=d z%UBjx5GVP>@6~`Elf&2Tsa-@pj(EbE$YFS|*;@semb8T6;dfwY18XZHkI(y!Tvv>z z-22-bX--0Zld7Ut6C=#_$;nBBxo;Q`4iC8xk45~9gc;XHA39+X(mQWLaNmQ_X?_Et zEh6B1SUohps<_j`wV~LeeO0Xk%bUhScu*fKbyT5pS~NHGS+I-pDwmE221k4_qTKrS zV6>#E&bqKH;O2$=wQsXjs|-Lh;=w_q`vD}ygMJwB3yfwWXwKfx?(YYtVWJz8Oenb` zUMKR+w-iwuo{J)ZQ}O%6gJgHKOMkH!k3jm#c&*tCTM<+9(LXheZwyvp$brI$_|ijB zZp*RPh1%r-&y!Y9KXNx*A1wWdEqL0icMNkJZr!KZpZy;4Q(V9HLSr~zk(C5Qtvvhp zhfIsY7w8n6c_s9A&+DJx0$z#>1;Kak=bXy>K%~z?4?t#CyWa22enfs`c(eL^EXrB@ z>GdJ!hIoKm4cg6bFVk)UPDy*o`PS`Jtj4I+?{Ef5JE=2 zvaQ-t&^vezCaW-coK1`{ba7B<6t(Z>#$i&!A2kf+7W=x06Vs{ zKIus`_F>1l$2pq+2gvHc^x_t?d;&`@B20k+pb}h^&J3yn*RLn56n=!p(;GH8t7S_c zI~072lF0VQRRe{PvgA9dIs`?W8aRK^PBTe@S!~Ku}L`CvSq?=hf8D+EUJiO zFqaig&Y$kW>2!H46PQ@)?g?qV?#e;zB=Rd89h{W01>;e~flkBkBq-j8iJuXEh+62p;3mN@RCYTGx@-fCVM{43mAbvdY>mFMZZ%A% z_-dN`vF(hwZRJ@!ug$j_j#&Uu!4;838=1%q=jQD-gFP%R-9A?)_0^lKUzYS_$AS9q z4Yw>MR_?XYjQ9MkH!`wbt?mLP;8QVAn5^AQ9dm11E454{Wo*d2a;z823#s3*pt!jG z&jw|9GiW6A0aU8!K*2r9liF2VsnFcz&I zC#eoi1IJHHFPzoX(OJ|ANpJGo%{-zEwV!DktS>nVjjJS6;UYV{C~Fs$2eWK-RvY7Y{hyUp>tBH2_U{uveBY_od9 z8aisi?bV+ODT~C$$CL{|1GJ@P?@3kSuvg=YRZ-j6wsZ^kP@27U9~|2U0g_b@UE<`v zot3{lzy9SzK*D)=Pn8)M1`EJRus_|cKf)`7Y+hnc-6Z|~&u^fA$kj_|{?o!CNOl45ReUI9k=`Qia4lts>umXG-;$=TeD&^Oh z?!b?92EYbT z*7}0v;pR3b)0l%%aV9wnb!Lz78O(d(V+dLEBnoq{j8j361D=1PD?}ZY7<0PWP;{|1n zJhk=@qtbqa-#>D0jg@IDh!)ysx$SX;Kj4eo(TDUPd@f6C;1Ydp>#ypFI@1`-$pxIg zKe&7hs?TKx^*JVi7g>Q_o}8h97`RB{YuAuoU7h0~X`PaH zP|%)zzQM!vY%xVT@sFa}zhCVPoyzBD0v;F3g{r!J$y7bp?{CMy!y%D-8x#^|zq>oz ztbdO~kG?07lVndmeiq79L%kn?23S}5v@Xsu865}i1lu zx%`D(i-&DOt{m@Po#$2q#%cdiqgIK-)5+#N z|24NbO4>ga1%Vkx%B$zJ+ReVwoft3Ld?^c1*BmEeR6>MGG%+?>kwzJ<#OQX$*Ui0k zhES43P|jyT$GI0r(sKR&?Hp7JEfpPB>D}nM7md3k7ORlR*@K63Pky?#G--0R_d6+S zJ1vsM+M9`)011=+tAYT){#l@S*^I~X^wXPRWw_)|+Rh$&&OE7*kQm>fL-j=iCZ81) zkK=J*ieg7NpEL+83;KcG@pOpGOx}e)G8vqFzmB&|ihX##k(SQj{NftLYPKjo!^R9rbHMt7@oue|5eA}&XFC2O zOmn+;9M%Mb?8garrGFPqz{gygczy)M=+xTI$gMIFvx8xVy)qhWY3loEPDL+fVi*+6 zXQyh04?Zfj57H`9@9(0$l>BJ7t$DDJaj8}W!}#N3avY>j=3)AJfD!Wf-yZmn7tquq z9_V?T>$1hiZMOg7En)dU2L?-Szq`*jEyob6kOcZ&y+Z{KxD3tSOGc=y|-$4l?rSjVg-=-(>MQTg;wk&{X4w_y(Jz zP^9WKy$ac$YgW*M*qX{M_s2sgf`4=`08ohf#Vtl!sW5tCR?Rq>NGh`CF~F#@N5=xe z7HC=i&a#CQhW+>V*d~Ft+MCSoVm*5A`YmC@M_?=x>yK8uTAID2IO8RrwZ)>htdGb^ z-=^X~L1%QH?}$vH4#uU3lfLu7{ZM6{3^gmzO04`!7la+QzX63;S?MC4MbtBtq@vFE zUjnK^0P6PB#Q=M3LqNKXB7T7v3ORE7=eq*-r$8JfiQ6SvGW@}))oPH|dR)lgGCxrlG?0YL|d0d659mv6U2`Q2~Uq5oYZ)K{3> zV(u#7%mmFqx=5@ar7)($0vMHfA2FpM}b35qeA zPA2*}DbFp9Aa69`XPJ(KR027^N`Pnh905Ny_Pd0gZ4lD=t9CWUw2pU40$~8Q(p{&M z>fgMoKTQ`1J>=1Psio$;&#z!Qob1B|CK5?ctYVOZ-v?Z{@%JBt@T?$<4?iN`LcK0e z_AZ^mhFY#!+SIGFpW$RqyW_@&9~~n6JCKYvFkp$^rD(+WA}^6AMbx%|UO8F~T(q_( zND98D5;a<))zeChG{g9W-;AoZ&!rZ*RNbCgVWI%3EFdtm7m*{x0 zZeB++_%t-3RIe*DATUSz_yGaUOUb1pP<}`AfUQRZd!W;@F()!3go5vhCg?jP$CALw z2^yEpoUv_D^#f}hw>FT`^X_QvM@N2ijic*2DY1wY^)?PLaF=^rbdrB6@gF9~A-l~V z0XHkU;M<)T$~NDGZ7A_}gp#VS7P?%vA{todh23HnzpJCa$0K6_EqCIVIwH;+sea^y z5SmYq*;c+-#7h@MyLji%@WppY8t|7snu@vX_QWm{PG2I+Q@W|y_=z7nVrkL`9 z&4K?GmF?}#dZYn zUONju4LFC$8CWCr07>-`Hdxp!-J+b67}zzc_=;|R+Hx~}R?ai{&*juT%~T0MwVn^@ zQfo2L9m4atDGv;H!f>uw6nIZPtEKukS)@f#X;`AfcVsF+rG@lrM_jkwTGQWWD8)j% zM5TS=@tn&oReyd*lmLAEPdqlY6{Oym143vMo-yA;at-PaV`wC9C9LD9gy3MF5j(6^ zQ6;z^ZYkjYO&&lpPqEjNh~8ln$DLV^eV-9{v!YOgI1kr{NOIDELtM;qi5#WhKipN{ zEnblNDj71uEx3ZIf>tOW5JfQfv^Iv}V-Jq8F)6=AiC)LG%w)CsMtU;`LOOtaVN{S1$B&nKX9i5qJhf%4v1;?zGPps!#2eLtn>u?NJm z+wg&ZDwEX2vjgX~OA_!yijBzRTf9A1PSS)9Pv_lMmXfK0O|ahIyN3o_)6G9UT1y1f z%m*ESu@rqe?`PcLxil!Y_+#$&c?4^^r4LGx1_VhkDP~;xWn>V@z-nu<4ShRhXMQcYI^lH1t zDd^9Q5JZX6tkJ*8Yhw=0Q;Y%lhFsL6&l)8Z%HYC ztMQ*ZGZgzS;;`U9qJ@G(l_BkP)_J!uC&ANttm$1C1&_3Av3*7?KIR=%&jkJ0hkfQi znTMOb@Q%+Z7-I9EbD{PE;(P+J7@Yq9^>yW8NoHMqG;J(fu&G8rx4sFp9yMA+wZBB$^q zmTz4sul-r4Zua4Rfx@2vUcXsGtSG~hf&>Wr4c_<`cDDI{j_nUmo9G-2>}Q)l?*m)> zD|y*?*7c;NDlAuKHM9GSLj>~(YDIB}EKXwN)ei4=Z8~}HPFY=KTEuXh%REk2a&ZIX zBRtX8CQfI6V%t4iGvFRp))paEZs{CwBVDxfiWQFrgN#jrvF1pY-Pz7Od#OZ_2DUwK zzxV-ZSxc^6Kv%{XM`&nx$P1NDc_DZ8st+|-k!U6I~I z{QK+a7iOk*c@=hlq-UFx$Jo^e4o&VE`f=y&J>kEEco|cqGrB|L4Viwc*Z&9F71DVd zB>3-UMuX$CIyfo>n(^ULmGEnu_F+c$@FrZb6Q#aht)x$$c{X^I{}5=`;XD9U-t|X) zi@=ka-|Tv70HO@EClUJ3mqi)=3oDQ9I$!ADaCrnhG>R4^Yg>AM=%tzAXQ!{k_Os8t zXImI;n9gforIxf++x$X)DD$zSN5M9qOSM^hbvRf5B6@g^McBL9zcdef9$BMy z$l?B+u?Xr}>O1L5H*iX^K(oFBW$5O%^cR;mroA`ui2mUuZ?$TkvD~{hop={B4zu|j zmRd9i8rxOhK6FakaZ6W+=v5u`+eAt4zO%<~_;(Qc9kgT8UcB9tM@HXaxaYw6n89wd zC$S=!j&;<{p9_Bez2D*BjNYa62cvX!%$9=|9R^jfzd%em-=Q#7&G^J-&;YqwB_n$6 zYIyeIvFNk)(IBXTTz)#Q19^MIEzELML$*$(*ZBe_>V@+y{Q&oR- za-4GkJjHI09cs09l=}I?^%W_4Hs5hLr{O>&K>OiXx%C;R%L!N30+;y;Z7)U>xXf;H zm${9T%ZJeXPS)rycYhREKkfVMAFahUT6Zqmoh7z#f!o9K-mKKCD^-7~&h?{!qx)g& zBY9QO;zF)?r|}bLIJ@nzc?Egd5DUD6U#oYD6d^Yytblvk-v&CL1bw)$KPtPbMt-8H zb1dAbWZDbtC2a<-8B|OC%M9BCgw%HSnjMQ@I@JYK{-HrY1NQZ8*LJQTFB^W?3ToC- z?xRcFBv){jxD6yd)m$7D80A# zenZ(^`*cVN{6Rn1!^ZOo(<4Z`561w2MlLoAur5E9 z;SK)oi<3Z)&S0)8`J%=pEfra@vz_oQ9oL(#@ng;L)L6w#OcS}geeaC86J1E_cv&t% zg^D_j6~A!FR>dabh~3~@dMscXy8{5znZH{B%<=wrOv=j%X^ilP(apP{AZe{;VyeHq zPxBsD{GO0+C8#d+wH>z6C1*VX2pLY7JOd`rTzIy2e!K z#AIr8_~5b50(WTA%&O!?yS;?F{wJ!i0NNcSJx^En7ru>g?~~%R!Gi79dB1+P6A5+d zQ-*=#qZ{{PCFwKD)-LH}f>B-)Jv&3$hyT#}?SKwm-25+BZ0nH=nYHEm#VCpr!B$yo zyGjXpK?osZa`nr-rwGz~LE96)QYWmC?`6X;9gCOV);=ML{%)5t1xfS9sA42Z;?^i-zT-|58LH}z{^Et?}87|w@K+fZ% z&T`5!ZW05(y$XSOw`YE*^_Qo432_T;6t8tFLRLhNY_}Y$$Rg6io+LL~XHfkZmQ*Vd zxkR6>PxbXl7GQm@J!LU$%wK2QSoplb^5Np{M)57xd6D!qt2(no@2*_U=iEw6=QEeJ+H%<7`}0>n7amQE6*zOYIcd4Pm3A zpCietV!wpb8WGZ&t|&)FFf633uehlmMGZ9>_*Az$$iJ##G{PrWOivd6&ZLzX2u}sk zh}4--MoVqWRy`kTrpA~MDRz)Fhw>s>)so(B7`ta`ysVo>dp#y2SE6BP>wRp>Fq>mZ ziSN@Ko)K)X;)d|acmexWPtz~xrYLyX3r=#w%@P7d-nJ)*T zL&<+;j9cN@XB=-a=fz>7Pu>jB3u^dvAFVV%a;lD%TH@xDR}wF*9sCsVC|;_KX&pLP zl_on_E)J7bNMHqG!TT_0QlkxbD2Q6t6evDEY?<7QLqAo*pqbW2lGAJy`7i4p}k!#}SMUlCo!52_nc}Oan+&WtbpL6Jt zH2O;s=X};v#ex_yt0lw#k9g-@@dVyL09qCjAbD92D;KxM%DKV`Td{4VkZ-VeIL<2S-K&c9>U(~3jXA;NWlKEu10nsX1>)*MBuSnI4~q$iez-t zf-NIv(H*pT5r3`*N0L#?jx$q3ymCX5j~cRV@iTvO<`a@zrM~?9_+iPPnD*pS4^j!P zYYjfRhwbXkE%x*Ce5hlFq;x@4S zO$c2CEe6%iyNl)^sRQ2K^-ZS8ZuAg8BG$Th^%tSwA5~c8Mp05t>gQzz{|za&7t&2VAB5qMTK&m literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/linechart.png b/examples/blog-articles/amazon-price-tracking/images/linechart.png new file mode 100644 index 0000000000000000000000000000000000000000..9871e62903b0495a005fe68e725f0d4af2f4c56c GIT binary patch literal 254643 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%0Ae-fQi>)>?~^3m^gDM$yPl?hwV0gWZV_ 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 e=bG!67hF@kt9vC}YUpQi9cvZIE){x(7U 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�fk;sZ|J^yOK>MZ?S>r*^l=pl~^x?t4G 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 X`#JFC^KFi6z@Mt3mcl2w7jOR`o3XRx literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/new-alert.png b/examples/blog-articles/amazon-price-tracking/images/new-alert.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf734bd74d96df24db345a07773c27b8dbef613 GIT binary patch literal 33908 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 VYijkhUxNVuBt_*!%7s1z{C{8H(6j&m literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/new-server.png b/examples/blog-articles/amazon-price-tracking/images/new-server.png new file mode 100644 index 0000000000000000000000000000000000000000..48ac74697761e5fe53e5a1ecd02ca666603b7c06 GIT binary patch literal 103568 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 z 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 z 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⪫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 M>KUEQK4Tm7Ul=8BxBvhE literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/images/supabase_connect.png b/examples/blog-articles/amazon-price-tracking/images/supabase_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..6ce0ea89244c38adab1dc11522eaac61adff42eb GIT binary patch literal 163039 zcmeFZcT|(@y6%ez8k!J+(7OuKdy!D2sx$$mcce)dgwQ{vg)X8LrAZZOh7v%Ubd)9_ zy@PZjM0!6@zP;AjYwWe=obGYXpVKiIi6O}w-uHg)^1H4lkq`3$0ktjqs-|1$VA z;2^V3E#QX8?Vh0r9vQT_Kvv+uNH=^qw2puh&^$`x_<<(;NCG8V{sSeok#Y z53>=eu>FOqN?oZJaXQ?dPkeFf645<*{Od3OBDlf&r+4Mm2=eW3Cnfy(EB^PpcR{$? zD%k$75BV3t1Zx&W85u(5&HsL{KmE|)h-Cjh;{SMg|9w{fX%PK8a{l4z`FG^}!&8IH zi~o+Ce|UQS|A-uUt?Mb4;dCus2^@aHuC@>Uyn^Ktlv^)HSCR8wjrUG z=SM5#JG~O-h=XbG)KSg~zYF9le(>dMlB|rL-|t%Hr`i%PcEpm* zp6^$;ntD$Uhe@R7{(1eh&;0UfzBNMYsa$GvW{c>MI_3V1-`1x|m%6aLB%_b-@uve% zF!SFP$;n2|g8~%i$!`2znD>Z!;5$9@anipaLt1hH%!lVETjcMY(T0JT$Ui^$>P>uN ztZZp)JB>N~bWJOWxSO{^UF~^@cl~8@xFGy&cZjcD;?>{pcbfcVbs(n|+sSe<%fOPU zks`>(Tl@1B^+EkVytMzlBWO^t=pw;-O+O#quE;zqZrRZ>k`idx)uEO4a!Psj?RIMm zi9nk3W9i{X0p7dHq18j0(qStZe*Ce&j-Dvsk()Y}HF%lPqi+Ucsz!frcxio353D;9 zX(TvK#AAMdMM_LMRVWrahNoea2yPIHN z0$FUNRw$iano#|A3(4LK@!k5NHL=~nJHg?V6s{mX$b*i$&5v0>|N6X9egfGx3p`qi zT0w4$k6DCl@0A6~#4t*gFN|Bq%Opi&qujaieT2A(9*)wOdTM8QDX7^hI7y1IjeLqto}JMg5wOKYMM2nd~Vt!;GC! zZmaJj^y(uYG)vD!a%LE~%QYNsx01I~>zgE;fsn+sYz1AMP{{n${@Tgtp+496zZ%98 zO5`H%BF|#!ANP-6=5B27Pr9xsz!^ugP){GnBu8y$1t&L{K^HWHB#R0fOtj?Ac2hg0 zx!LKNqsI6@B|!wjV>>|&gdXqQyW$;FHDbhrTj@S*89vLY+wW#iZ#B6r{fw2Efvlu^n~hmU zFtvz{no#!1o@{O}L<@|lKmELxPm-e&OWe&@ZL%K^hpym~6Ue3JHDEEffQzUv5_>4P z_h}bA9wB7i@g&|h)h3BotEdERlwz0R)6y-_#K?@83*PPzIu|i{KKz*4c)g=?aIv_e z&!{=7PoAFE!KI>C9G9$vO9EMZSKsquipm=Az)vJLC@<`YvFmV;J_q@I z?-`$!-jMQmi7opdFf+uwp#R}ETrz{JsA^C1jRUS!;4Tw_+&oU+Im-JTs z5a)xEKm{4s$^n(&%2iIpcc(nQf`GKfyDV`XW5I{sxPlbJvrE@OaV{i2TyeV~O?bmk zh%o@p-BFNY74_--c*Ab-3!_I56s%FS+tM5Tj;w&VbjHyrOw8|Gm!~K7r*aaupgeq9 z|5#)j5ZtK6B$(8O{aIS~ix_d&dBnbgr=(J6q=}ZHCUCv5pd1y7cE`5UL_|_@Cf!mC z>>8bLEMtmN*d~M-uogESulv;NoB7;?AFt*dRdN1lOn;5UdmH-5p!2irsCLmqUPt;Q zJNv62!}vNdTN{=v(L}fQ4%Dvx4hTAIWoZF*EyZqLzv=k#;UddLig~rK@TiFgp^{rH z#GC?^KpuK)c(avUt_NK&vw-BlU#Z&6j^-!kP>t((!TQ43qgPBMVX7?XY(qU%2Sy{* zJKA)eP3{GOwOKrNnV`p z3{5`nhh9SQZ3kavrlmN0Q~_E6snb70U{COYuG&(2_U-ql<*XGXqClNkj zm=@`iP^npaI$WttZ0Eo#JT5?hD2|h^-=6anrFHCZX_u9GC_oA zWf^z}opR^yW8Sj?JCg5Q8+~rMbi1CeT%Las$E9y&$TAkzxxwM^An?1;{*|Y?c^B8A$1XrVNa-m zaFSp?6&aM3G6FSj-=GCl=qY+bda&-b*pVe`tF2L#=kx!fg9x(ge8b zX=Z5uIEhMM_WHttj642YezV<5>V-9Vn|Zn#Vvi4(C7M!wMsu{qtPyS}Lz(A~bPt1* z`3VdWy^cS&y`F>k)}x+JxP~t|a~3Ax|K6n>@j_uNgeyM@5+AxhW*zUgmX~a6R=R#O zX7qNO;GPm9Hv~zXK$&sY;55Bf5!^H~erl&7<$@w^z%ld~t>EI5Yi)S>ZOu zy6E)M%xt-L0f_jO@lV5ei*-s7*CS9ANR)WrVGI>g` zma9|KIV`?#R`CYys<_SvCTem&H#HdyN|~E^oZ?Vt#o@$`#HoVAT*J~H= z32oiEy|}*9FPG^or9Od#=CHN2N#Dq}(fG51eSyL~nbB9bU^hzemHYtSCuP z{U~%7bo)=%%mVg?shgpP2`;-YArb^JpoiCh{Q?W?TU)%)b^LOzpyMH|@XmtwR>Q&a zD022KN;9{Aqy<8pgPTDdwGhH;WXw)y(JRedrADAtsQ68_Arz~>ar$|qv>Q#N{4-2j z0Kv-JD6Mh%(v25`_DDikPyrCBO#g9(@u6l^KJ2@B>9}&1I;z_C3}Zyq`l=7~G>Oha z=~13gk@DE}71@K@)5p7N^^Zm-CZ!1ua6uF>)Is&xFFUe}x#3p8M;vaPS5T zsM>Cz=UQ<;_A}`=PfKGx)R6IIw^D={f|C{&nR-X+=x5>%)pk}YapK}-^tp|C)D>N& z&rX=MsBOggW<|ek4DHoyws7^uodJr@=-L3@;qO_o2hD0`mpB_`as zil?-OeKK35qk!o#W%S@EMTCl?g%6l7+q*Z%NNhfKJ!GJPuwX?~iCHG^m1;dwLL;r# z`iD1P>uM57Ond5{(+<#Eb*XHE@FAN$>hpf;Vlz3rXk^$3u33K+YHvmE!sk{D4++DT zP-*@mhXWN^e0t%B*XQlx)x$B^4KNn>v2c7)%qqD5M?)Ss`+}Yj(%Wn^U|ulNuD~T_ zi^)+J2D4-xH5tjggXk47qqta{^w~%Ap$*VWj`gKpye$^qWov=i2E{L#)9qHQ_Atkmrt-5-_sF2MW*@TIh;L}hNP}Or4ydv) ziBH()d;-NLqVCt{BD2>Shz#^?cs7&I&jhliXbeJkU=biJhA4d)B!Lb;?}zNgQGY@5 zk(AR=L!D*D2`tJfW6%&|aM0#$36eoSJn zE=_F?jquJhCeWpcMFt|Hx4H85cNda{Z}Cpiqrj|HV0lTKV{K101>h1HR`%HaFPF)m zm|b3cUc$kQU#L1EqvDrsTD4w_0hNXnDUg#ASrVXcwDrF9_2^HRJq+9XMldKdg;l$lw|Dkwcj!QB@O%mdsKVWSw1hnLKT?1lVm>DB=6U{ z#5+<8X!0kSJo|bY^^6&?z@z6SloQ64hIZ*<+gp31=KU_cLY<7kK?7so-c>RP)h9gt z!&&JPk*H=CS4*_<50%)5REwwp=d#HlYKMl5p=(R=7-?NsRS5F)?KVaGByJIwpi}JI z$@{QqLnkGq`7e;rtUjI6-&L0xxL1PV-3fi5Ipe!g@~ZAl^?F`9u^qDZ3qDC`c?lbj zF2*m6rsCX{~ux7skgOML3mK=F6G^iGY479I0QBOu`&#E%ID;-X61tzO@ z^32dQZik)vF2+h+bJAtR)GWr4P~K8UCXb_%zdcGo!T61A##f8BZyf$i)Dovk`vmdG zdH8HTjHVAQ%J0jePU(2#;!LJA?%q;z+`3miz%J}todJ2p5x4(&#J5#xX=l{OZ?%PfJCe-7apC0 ze15GTzN5};i-Ue7{52MV4S)}TMkngzWcDJ%yfe{^N4H0xr<2AE@&1iHS*H&;Kip`V zQW`8S?}rG&h9~3d5>5|VYQnuWI)XI~)-Sno#okP}u%rLOhzQn$vx8X{*DBA`!Q@6N^whKNPorjjHe~aE zUB1?Z&I$m7eTB+h>n}R}7P0joXqn@EL&l?KaC8st27XulD7UgAk13&T&a0EjyYjV5 znJPDrqlgbby3oqEH&@vJ?bpFG1HjAb20bo;YG7jE0lS0ika3~v>KkK*$RfHB{D zVV2mtLgel64!7aB_U-oFkL?>BXTD}^lL4HQAB8h=F->}J5%8D}bVXU?@zZ)<8Sz&a zX0)SYPZ^t?r}9(bXGi@({Cv6%@0{cqO2)jRoO#0kI5b|$I})r%Zd-jUo-eS>p)IRH zv$T@$x_1#_T>zWTN=dV za;I(vV5YC=L|P#@7xGkMI$lrIxGxTN-}OFkqttq06K{q;p1lh=gX829*R#mINv{@} zlrI1^U$@tH>W!wJyw|{6Fb{2iL$tV_r|lm&;ZP73a_tV6Qok3(Byp}>PS)~%+CzzhZM>#fFPcQ%R><%*uaK-!j6 zjeHC72VqeAMW9KlOOt{%|Kkr(bsAromMq|qktKk8+|zNwHtO_%Lbymyvz)D}T7qeo z2!{(wMh|)_KtkulX%0StNv2-oFI`Gf<+!8_ts-x>`0w;4%iVs~%_8cf;3xOHi(|)N zH@YkU;|RcwSJ#&5v#&SjBjdwTY(_A`^L%_&o*PrUfxa`{8Q;ahI3Zf}xL;!YNutT8 z`&rql-#$Va5HHc`!Stid035;#aQaDBfP@u91vd`A z->+J`!*%_p3Ado)S25Lp+>_r7wpB(z@r`mkSH7&$3CM1}SqvJsZg`aw*wuNok~wQP zse>MP(mnC$p}qhQ;<@ztha7x`Mtexq*t&QXAUaYFq>!d;)tacQVI=6f^q}*jojRwg zi4U9&a2@iDDG)tAr4%nwu?`&j0Fy~l`axO33uQp(M9towz@Zv0vp>c`rsLZPa)6E5 zB!wNW5?GF!dh?M*L=^wf5wKo6)9ZhyA?K_M0F?D%pLgTX_o9ofi}Nytl;>a|5v3|i zkjEh`?)nYwvO@h@-^p;V3Ux>kMY#%t*waV7vXaxDLxdA5W%S18s#}oG} z*i7C6bQ~y-$espPB@XKu^Q8qIuUUqWiGP6n22vC+7zr7u5Bfkr6VJ~Aip`AB=NtG2 zP8UrhfGR6%tX~v)C8fr2OmQ2KuqEZYHP@?u^}50|w@btAxJd`p$GtwzUYsBBSOowe zMS@eR+8cJQMhxwGLj&BS^+Ppq5SjjloYo|<1ZaGRP7OIV-)dxHUeAK)J{@)Yt9l57wj#6j_EOq{aYuOM82~%CW5c`Lds2k*`WJd#k|NI&UAn))25?Bj z|9K{dLN(}t8v4)7YJW`-)m3~vO*(PDEGHNTZj%B+RCV{kRC zkk&OPR{Z1Gwc6sCm=Z*5>-JC8Sp-OkJTK0FCpVFnHJ$qgx`MJ$l*;eG2-eDx_!r2- zHF6uh%?;(UFdO%J&Oa^HJp^Ns*Gd#CeJ2Mx+}f>EX$3T@X&al$B62Cny9Am}pN-jN z2DE^arq|3EJZ+O~3Off;XUqJ11SqP4U=7>M6d=MoD}k6M&Z=Mt#(bUy=4V}O^P!ZQ zA8I-YTjbzLc{7_pQfm4ON#N>R$Qtjg2%VbGyS;ZS++rOtx6Q$S z8;^Qj_p$0>7R14@{%p3FmM62>({8O~sV{|RGC?V8`O2A@7Riitfk`cfxdNr*n!bs< zGOdu65-GDw)7ie;-mw14=-OADuxNG#eMOlO`8{5I@RxXgZ@$(^dyc*+T_AOT%x2Zn!ai7 z1xX8Nc2hY+K9w20Q>94lBLuA4`w*<;cg_VG6MITukJR7^e>%Bd$1e;?(^E#EJ}3*B zxzp6sy0FHz%IY!d{i>CssHOel%|0TZg4!(aM!$*1E&h3lrX!7B(d~PBREp4f(7cdC zP|E-g>Ij%Xwzs|Zf7NXz<;T-Tlo$wwJ_gYIi9-#7Qp+4S)Yp?Hn>@R}w*QI2wcb5! zj%;3EhWsgH6DOg=o#CA_+g~mpJ%``r3Kt|6(te8c!@;O8dKJj)a{17H*-u6N?iGC$8W2F2Gk*clHJ4<+nmnI_=+>Jvyl3HmC1C+ z(pv6gHUB`sA#Ji-5$Vj(#60#{0AoI8lNVTa_tlA&qL_Fyh;p2Pfq{Bauf88;#7yx+ z1_Z!(&1Z0N^^or(Bf1Wd9C34%KPYCLYv#iPGT89ic5bs{vV}V4+y$m*(6r&T_yS2b zUSNU<$FX{w_&;CXl?2;S5v=5FW}L-CYtPxW6h}DBLL*ikK`!Y>|4@=2C}x{GI$e=7 z*AG?wj2J(eT9F;KGad$l*eyzYSapk1&F(?dMMFNMZ2WZp0peS@MLb4Kck3YpMbR@y zN1X74yG4<C?=~$7XzNP zR*GhFE^=LcZm%9B*&q%*kgLl+K|t4LS{O^pmA&H?0%39`Z@Tf`u_elWr&ISi3G8 z`_RawEu6hV5nOU!w|rfG0a}HRw_7+!7s=+M&PcY9Htg^YuC|aWpe00qs2xEl(*7yj z6_kQnT=(;WFFF^p2Bu4&y0yB%O?}=Wig{k4elX+bI^bozUcx0xv3 z4W}S2-xMJG!J`2G9XjU(#&L2mZP^ZEY_Uk>b~37D#2_&VHM|2(FU9Oa!?ms8AJ$@Z zzrXQRI6r}I4U0^$&##&v%mxJw&u!ZnKSLMhpZyS7kLL@+W~at6sqFk6$DbJbG=AVjwi^Ax{y-jiHO^RSKT>0}9l!?A6Y!5qfSXh(#7D+$e(mGn4` zxxdh42X(V5=OD0eoH4PG>pDSuo#Y=^6!kI}r_ZyU)$v~Hl8KpRax$;g5$Z(z*wFfPOB zrH-MK(N}odYJeD_Pb*@pt~xyGObw4rbJ2(%RfQia=gb0a{ElH}bIwoD6($oIkKcLD zTCBThBw{=S>yAuT?9f|J;`67ITRi|~rEc^`zHrxFUyx<{2A{wqt*Le{1;n^#J#IDS zH|CH7=+=}*1N~NY@sFX}!*_ov)KB>Pa|3j75AQCm*};NW&E`fgI;*g=>`J1rL9jd@ zn;F~c6OZvY3fJ;ri@X0FVgG~6MJq}$_15eAqc?PKagjjK(GqaZ<|=V0!Q$bFQA|uIG^jIwY_>NB>~U=-J;a52Go&nDP#_Jb>M-altu_p*|SR-t9dj-B=QvpR{? zh1#j+qs>pPl=+OQ@};d(Sw?H8ZQv10|Rgq00ztKS|PPaakZ0 zwm?-G6Q;MCdEedqb07OlN?LK?(zt*0=5Hs%Th_nqaR!zmq>yDxC^-0y?_9wS1?l2F zau=ubIM+*A{qF}k1Ehc2`+~^3k_HyXr80bxKh$#mOALpkexVAH+F3UJ!#U``96|6q z0o4MTC)r`(pP!8XcscD4+)LNjMRxwnNBOh6|32y;C5V5Y;r}5c|MyJv?`ZmWH2oiE z;Q!mo$){f>-CZ~-NcWl$-02aCNVR=?8;33DL769aZ94Nx0Ui|r+iLTj%C_rjp#XVS z2YLXIG!M+*>!SxJ!bFu1EP#dR8qUXu0G5On9PX6`qn2Kxuo(kEafJ9BS@{PN-cRy} zZJe?ROUJ|d=41f<46^dvy?B!BO4e~}`3LBA^MlORjc*7-Afyc9yu6=Rvn`VhZHaI# zEzWgbm$b2#|Bjy*IFEgfaUeQH(%mX>`rB8hie4Db%geidvem@G474A;S8HpMZtuX5 zh;Or0w+^sHPoT)2XMS(mzi?WuvR?{ATm*M39F}+UGl2QO7T2BZ-|bttuJ8iq%8U+H zeB4$6d<0iRMaRNu%xF7e8Ac=CNZk1DNMO)&v1upwF4FPqB;cn^n!v4V*1h~BlV!uQ zylf!~zZBI$U;AkZJ?Wy0)W5qjOHo*1}_~n4hb>J;4f-VBrgKi2|5`hsIW(h`? zK31G~Fb_!hHORzgOXL}P;%O0l4OIDhj5izm=}%6%)V!~*$glU2oheno#~6-trmBk# z2L)*Y*Jb)VP>_42eA)j6k_x8}z9>y2t$KA)P_L^gWn&O@w)eN8Qy?_?Ju4$Ki#Q;~ zP6KNIC6NPXgSX+sEzktZs8}Q@!@epbo;c%dpY_%C`xA~bNkdes`rrfi zVe#Sq9KBG+t>E{Xcv_A)R1s$hIKo#YAKU~K*q<28@d;d4+n{<;H>vI3e^F7hB#1~I z*qA4|aC3d?m(pV2F|Rky28$!ULOn4eeY7g^%<0Y&&w-G;w~c&S7xupA zi|jrC7jft+f&Ex05#{I4)Q)9CHVj8ND4!YMC1AEx8O-_Vy8_7`bY93yTQ1O;%>OLq z@;`4at|WbVNxi^UItg5bJW5_j%`u%wXC8BZY`g!Z*L=y_;niTIvzw7xfIV}v?ydlC zM@jWGNz{f|q`WO~+J2o;G$WVZ{1jL30lce3tr6t?gLl?)6LG!sY=WnT68`k9@Ai{6 z?kOcgK<<0vEEdsO2>gs$lj97p*B1HQaQxwSpSUlW8N52bS;}~l<`HS!B47q9UeKxy z`307QgR%gJFx*OfWf-Fe`CF-Y^H}d|wi%eTaN8^FZ>D}_@5{ZPSk9-3v-JHX@4h*P zng@MvD(^S(F%lfdT2`Jr=)wW!j(C>L*`0qSpp)9*E9xL4lBEgYg$2@Wb~uDpSWN~? z1?#%j4Tz<-#vPbXt=DiF;p;@NjvC*ac6XlI_fxUFCl;)KeZdLSL@WWj+D*PFMpCC^ zk@ykCpuv6922H%JG@K_}-o8w-X4J9!bWb7}xSk2mv8n?8(T)vwEx?@7CGsN00BRQ3 zO8L*P-3^L-+9*=MuhRtqpW% zEecO0ijCL2ILh*G2fvoDa83^MuivyECKBzg^nEa#X+SL(! zM|5w*6LGa{l-N>S&_-V4llbxIg9$%LS zX(52#AO`M;C96lS9Jq>le$eahos-2Bc&<9MObw9AYUwxVUYU0sLjnmY&zwwlbZr-;731zcpjQrA!kz>j5!$t<8 z(1;kEg`gg|y2+A@c&BmZh8`efrHWZ+o0n6;z!0V3w~(i4@zoHCJHA`!#Z{gU1O3I^*9lo^M2 z#LC*r7c5hJ)~CH^dpo9#7Gch=z)1hJh|K$$Y_!cBxhQT3jS<=DXp5v=%o*f3v-{bV zFpe;#&x&yIa)ev?*-i-W&BQ(D2>Ka|4feH)@dLYu*7vKtzTrja1@h!>IqfsB#!mKn zbdVhLU8HoUNNnuItNb1+ag9@%G38O000-T2Trw;eOSra_S7z};U^WvIcv)CQq))`P z>ZBKT^ga0~BUi5a*qAGSN&BA-Y7cJ+{tQJFO4NgdBo1a<4&_^9KsWQ{Wgiv^^#p9q z4E?EN!9Wz&oC&nP9v+tH1!ZF(c8FYJjwUQbeXjv!WThG>Te^XDw=!C#+0}jPozttx zJ5lqva%ocBy6_c*iz%s&d#}|_2ZJl4N(`+wN=6`>9!HLn3Vd7eu0NT#1wK$xT#v%q zYtP%)oLv65b;)Z|71jkn;F%LDlW^n*Ma>j&p3j3l8fA6gqb%kUGf$rwAm+nF?y9}L zmQB_67PtzW(TLZwJQ-WSbrO81oxg6g9Bh>mc!IG3MZM@lfl3X3_0+RquaE{1EAv2B z>qA?Como{C*8b%EKK*&gW^W^DYf9~QGvDYL-8PojXf2oA$4Tp$ngL}`CDhMELTQ+- zuHvfi+1}^@v<5xoB>^l{#Q|96^?gvGwag9O`>Yngs%P%vSl}$pVGDi1uopE~o&@66 zv4Jx{#m@|E2B>i7Sb88Ax&h1>i+@!g=ddq2scx2**+-av~>rL_gLqwlxc zS-wv{2^LGVlzW<+NRL?rm7{Ayzit6v=@~L_ZOFm&w`|+}pUEiH+Z5tv?&k~`AY65| zMF1)M+V0Iz!M~G7 ztJJ0zECYa|{)r}2M`DI!xK zGhp>bmPZV%V~fez^N}6ZEnr93fZYtTKf2RvrJ|D5{ZvuC8i3O$J~2C27V&1^{w$G{ z6J`O{-ORq_R4W#{z)(NnY>+PbRZU>ImI!v5DaRyX8Njx`KvS<5tACqcS>9B&y2Ply zPObFEdgVUY<{v89jgG5mKZjG(xlX{bhi=#S*!!3%>Rz-^f)H|6 zxT?6gfB0niLlX0>cZ}4)p+={KpUfnXV|V0~=8d;ssqwE-=22Sz0;DUAsQun4uI>VQ zy0Mdi2x(Mf=}7SdHSyOWmz>ut(~Q+1%@113>=XD+(K^-{K0sDz#aW0We-GNN`P}Km z)`iUq;55Mfbm+xj$Zh`$Y|K_>C$k@~M)pG59ptRs&-uf}`2NNmT36Zv=)STw^9_x_ z6S3d#WIO)3@8yGv#XTadfn-!r9yvuD4ThFPbuUI)C6wwk{-TEKU590JOqc|W)!@5z zvy@mUhceX6KZ9RSpMK8SR-+#a8*#s7>>Qy`A4sf* zLkun8M5Gd3#v&ey{{E}W0<>aZVe5umj-4Kt)=(5(O;49{Ou&PNw7?M{xfmnzvC*}y zbD>p3Vp`N3&cB?lYzaGtM7U*-yntd}`8LmeVZTGtr0f{V6K#QVW+hheM}g&{2Vu1S z%JO2CXl4t}r;gyp@>QY7bn{L@|yyC3YCg5&v*0R2&W35Pb-2FpJl}VzpOc zZ>=CRgG4{d(#o(#%g`G*fa?7n3_5r^43#{Q7DaT=qd(M7cx_hRJodgbs;5?zdXk>P zX)!{?jQC>1CP#1WUxG~+kI3TMkIpvWciyr&dwD?B`h~VxpD(9>NDo*!rqw--NV`2c zq?IOi$=k?>l_0p4?2ac3!4Ap2M*1u@>~e-`Ay1B&<+EDOvYU}Um!Wj*u}?cr?g(X4 zcgvsORvq}@Jt5#hJWGYpjaFfLw867G>KPM*;p`~!QhEr=FpR@|4Pg$8-={mdqCj*0 z@MaZQrZI0~+=rKqW-`%rMrKua5}GROvC}b;g%GZAOP43II1RrA(9)F1u7wJ{xF7n` zP1Fn0^TXdHn$aG-*7Q~#j~>0v4`yY?a%JCEYTh+po)_lXmui&W z2=I8F*DBLy`I9GbbX&S4gM$2u|EQ`WYqAk%r%B@?MW(gQqUo{O^0a}R^0|-+=XkrQ z)Njle*XESbUAh&)yP;AGi$1TU^52fhr7y0U`_FD))m6@ENPt}&o-bE>jI+q(?#Rrp z?g~xN`rM{3KI$BqgG?MTB` zAC~XVQO7FR|FBl8pY4GLGeoDay%3*d)TTj5+^_!9>Nw&x>8#IfVZnCIoq^=xz&7i> z>GG~NA5mDWfm10OLf6{Pfx&|^n;1TH4RchF7LVT!4}176QbGhaHl(ZxE82eD+@ha~ zO;bic3XOogA=Is>olm_@JTTXM@(osc|6L_+=aVcu{Fr^uZ0Z-;dm{E%=fW_2tmbRt z%i9)8Gh6~@Jl2M56}y*?5ga35eM|ObC%m~^a!Ro&Dl+p`6ge2(+1+VP`mOd2{fX*3 z34xq70|%?!YxrVkz?s!BHxOvu)G4hg<4!r_J=ktpBb>oup;pb+DNSZos2B5j9Q=76 zQ0C|!1hK+lR;u@&b`GFVTzo}gQT%|PF0h`n<0I-t6PW4Yh6G@JM@^W)#fr|YZrgJf;dch^wC^(`!(s!hiom@o6FK+ zu3Ghn?WoW@VOre$acflZ{?@s(hB1YRD2)}<1wrrc%M3zFRobDQH9ahg)RQbtA&2SX zU8fUW(q#W^#8OO_KYJ7R6{(>Ob{@n&|NOy8|1G~uc@_N(kbRRn|l#Si5uc zzofjXP2XXD(HUk2D<8AvG~zCfI{3&0b!YV3M2#=5%5a3s0Q=%^=LbXn9V=@Ig!f=? z$GLe;Q=SM#hb64$k+ZSeAgz*9u0#v1DUiGnfkRneMs~0>tKYjELTLhpiaNGwH1ud+ zAnfFoXk+2cv|D1dGsM`K2+j=q&~a95xUy-&R<^3tOy|N=stby%^YTWGDYm{!kl?Ja z&-P)CZ(;0AZ8V6Q2h~75O6rl>NKkCZGs$qmz$4I`iD-;RCveU?%ycC}l zgOT;C6+SF3?t_wj;p0axG}bw9$oWa3btoYcwyF;)wZEWs=8JkxLDl9SP58K8N~LJn%X;<& zL%=1}t&BGmf|b^oXE_&n3pBq&M>}4V*{G^FFSxpAa4n|`YCmdc=f}XESqk-e>xbDg ztS2jMW3c?bDmneAn>R6XVu*@2lHrms36~dWUx%+SCPA)p1$|EE^U3d5X_jQ}!dcp^ zcAZSrCuuQ@pf@k39im`B&Gn{y?|R{vnM~#4>5*ao=BfvWRUX&9C&hOBd{G2e+B)P9 zAXPUv0={Q-CeTZ6>F4+s7j?&+pj2Z{Pa)C@2!__7-SjT zq9h3Jq0x3zF`iFDUCv?Y0Q)?kRc%K}Qqp@FDM`tns$%w2eHC)a%Bv8m0|krT#Qj6w z&!$VTTxhMv_7A!fco@OJi{HOgij9wQm{;}vcfN>-!SDS}zb^BJ4YD9zoq8u~?fF7A zzC+ax8O4qc#=xln>9xrC*r{OcP$kGZEw7BKUkO0Qa`vsGa#Z>IFzYGy%k>=Xc6$V( zuk*NI9J9P1@<|$b>vKumQBJ%cJC)T50&*Wy9dBQ&$s>_QJ~epc&|&bTl?05YCtAP` zK@4T?x9DMKB+W+MOKueCmEhF^U{2=}DSl+o=^{(*T-ayjO`U$O9jTQxUA7hu%^Z{H zwTcfJV0%&IGVuL}eOBp1 zFbWYR0s7Eb?44n+`xW+xFY@21V!i&N5HPi0qU-tK@tZETk(uV%V#5MzXwOJpj##Ef z>qIlmb}B?9I`ijcAqq28_Q0rhlNJrL11DBiwbYSrk;sX5$Y3)jgU_+O@Sxb#C5Fa5 z?fqFAu*n3BZN~u*qIkDQ!)b2XntuC!vq8MR07A$s55AGt>j8VAi(+7(c$7vpWRZ{} z#Ad`N;3Y)L&U>tfV*cvEEqedtZcKMt5-~k=9@T4$x)@9vdnG~b-!5t-mA~j*uoCHo z$Yt)Nc&1{hh>C%j=h!FPIn-N?*_J~6z?ToWr;f#7jy|-GX-3VmOcj{JHbT+Shi}E9 zu_y+tkBp<8*=@CVSn*{!*@DJ3#X@XJ>!HpmvjS;)oP(Qy5$8&U)r1Mh*aKz6l(o8& zXcsMI$yn2%ka+q5m*a7<W?4{F%=bJ-M?&4rz}}+EQAibDn#^7now+pK23L zCa~wnQ~!_jndyNDmm9`x(E|Dvjr0r&EdR7tR(?)2`QkXT*JtSww#ACY|Tn z3xf(w|Ee{JpQiQb{{5q`!D}4*iz{U@&Gpr8HT{8LW9r)I0L7YwRgchVDHoci!ywRD zIsSLhy-?40A>#cX2 zgYtl_5dY@KNY)K^zWny*smap6THozvQ*R(oc;PDKP{g}?H2c=|h$kq%05hk~TQ(xo$9eeeDvQI!qx=%+n3!;jLcJgx#?p}kD|!@}hv_ZBW!dOX zL~ZsoY=tRfv;tT?B`8_7RUxt;4d0Fm&ua@YFU4CMVhy>i6dvhNl`d-BVp4<}>oZi) zDPjr6N}u~>*hgGlIC=z0jY=JSR$weFT*@}+G@4RJK}FSWFJ!u0_hW}^$CR4@AfE&u zerinaq(FyYyS4Pr10lZUq#y6hkKw&E2T)(CFghWBdVuF+`z5`e$*K)4rPVf0q&25d z%FIEaAr{^O1pPf$#Q1B<|N@M*zJ zaCDIWLsE0E>5^IE%X?%mlM>`ykhAeUA$#yCES$J5aums0LpjUXbJ^YhXM@cm%TAzg z)9O;4T*uUFXU;z9=D2e|;OVuqHa{lQ?lC_$MooC@(qDDAE!kJ_UE8uH_-WH*IVhTH z_jC!m<>wHjwem}p>7ToEVku~KjEC~G3Fo%uFhlJFDu0R5-=bFjbZDlwtNM1WMcC@9 zlIU=#RqXQn$@!=m$H4=(?|*qy_2l7SYsn)Kf+$i@{V-N)j@I41qcUdn;;pDa`$58% zuN4Gl0rtbL`fCg3Cy($P7{nCWg<5v!*ykqj@3);`$F7a0sKD8usL^~`I76t@!8VYB z4l?r^g=S270mZc9Hk}L4Wwb0IK8O0dGt@gXUpb=73f*!Nd~G%@*kpa{vi*lcX@6EF z?iuOrdq&$3sN=RjpdUsaysW9wFjwn*TkdJpplbZ_CcUHBv{#MLd7Dtk!Du&&HUC02 z*ma=S%m3m%mx<*G{Fp4mxXX?zzlyyvTHpp5LERpdoAl!;X?PoQo*?W~eCb%!d!jtJ6t-F+7EC zb?QHrYHVZOZno$Tqb@PEYjtXYQ58QM29%Kigzj5 zH-TOAvwUX-&|r)7bOJI^b2=sue^GMPCXA?ZzrOckSBj7FPVU;L4+gH52}*`-Y$dCf zQRSo87phvT43Oo*xt4>3`BqF&{t`|Yrz6qpK~JfII32ZdF`cW0f3a&6Fs%Hf9#F4T z`K@ZuqReInI&@9n%_j}{YaJf2dF(2*nwUcl_Z=qtw86y2k>#{;f&#=HR#TdcLl{!& z1T*yrQW|P&FXDzKz;K?%#bmT;ba)PVkvoQd9S)7Es!1@LNQ%O6H44b&&l0blA^HvsDn&7% z2ODz3n)#~NR!V2)tvxeyO?$924N0^*E-~Zy#0adju=LvZT)g)iWKo2UV%VERp1$y> zmCoWu`@vo2?lX&%4H@^O__XenqEC!1vzjgqK6B65ulDr69j;b3B%aWr)!Meheu8Lv z!!a{VOL=%Lm&rZDrQkDAPFKDTYDKZ5M9UoLe&4&vc{|b9+U5Q(+U5BxCc0n-W0%F} ziLO?Sj}H}*dYzwNYgD}uc4hq$XVwwjBo&3Fzh6Nce%|Aq{!hQO%N{7W*B7vIn?n~4 znfz%~&8U}NZBSJzSACN$@C6IC5s6V(SgH~(zu#%KPQ}mo7jF(lsj`@W+*}C_R68t5UoY@A)F% zEu-~Xt{H|dYm_E^$)u=>P;4+4`6O4x_E+)j!hu-|B5b_-(hCxPNIPMts6WpNX17Rh zX7pa#0=pXrew%MVxUwNSW7;a3Pgq;6=W2@+T6J+2eAIVD`(uq2X1+p!*e>z6PCjv5 zS-K-n%@9F%3dD>?A?T;Q6?@-?F+SN$%sFa*7q%MyxVjs3ihLtv5eVoX-d*H@7MXDd9dLhKjY zPSeJ(N7586Rx>psO?ERt+;vxMhA^&z%ujy#e9a&qJ6w&OvXz@1F zccq?YD_lF}n|LL{$2vyUAiRszdQ*+iS7_QLz7{K!e#h$-(<~gvT{Gnx9XQJzq(8j5 zHlIyV_W!W=o?%U`TidoG78W1~Do7O(5Gj!&kuIY2BA^t3(4==Tl+Z*GkuEJXgG!eU z(n*lsi}V^g0RjXFA#}c(wV(YS@A2%l*SmjyKlVQlOeP`op5q?(7}q#2H1P?i?aij> ztS5D^9?pvVO_%o{mE*T>dEQq&yE;i(`fH%`?6tL)C(x?QZ7pwPrVATy*+BhJIPQD@ zYO-iJOp+0i9roDk;l0U`99{9PO<({YwNlOh$9(c9_NJ@@C#!~s)P_bYpKOT}*ZLmi zR`N`^JTy}%uUokIM-wwGf#umV-Y7fQBfNi!M!N|oM18<5!!cbJk3dnjl7{Z}T^cTy z2?&X2Q8ouXV=O1l$peMq!sWdL+nR_}@Ete%yI(Ydc_Y)k)N)6I2L(d&mOh|dNH8>j zlz1QixZUXOiYOD=;|jF}G&OjN)*0;k+Fs_VS8mA-;iYfJJ2V_zhN~QmD>UhT3ndix zZ>ZFd6#2Z>j$QVLFL`A3a39l(GApPF0Ew3p1o+0rP2%{wI5A|#9>gm zyFt$Z5rv>?n8^5-g_3AjuE=4B?N_S6=M#SNLxBqS<4VYZVy)b4NF{HOYq;&$kGw*r zM0f4JPlN$0j>MWzt-6gQ1#<4?rlGFaR5Jyfjk3*HeXS{jjV~K8AC*7v?{KFUR^|?x z2jO*RvM|xb>&186@0+yi7RC0uQix{`+p?>^@JOxl7`|ZD@zGFlB0?)mqsRJv?v#Tj z^9Qf)i@T~58^T@uu?+7YSdQc^Xm#6z@okUvirBBWH41;$?Y18;F|5AD}k2lMx5Zk=vClwll76teHhCt%)fxwuqSrnV_?MDKVa>a&uPKJPp@s_TjkZGyP4saaeIe=A_L0vwMGt*tIlVpbN_kdCl#q$}mP- z(%e$>YAFk`B{dXU#p%cXE(tctrET(O2DoH;-@~j#;9>JkLOx?wq5^XN!hzIME1AD^ z)57#}iZg=3jfEx%T^E>A7we zT4d8_F`V42Coo?h9IGM0ixM>aY87eQdL?Zy0ywz?#cjpph{f_G&(S|pdTsfCFfinY zxvt9Fiih?wS8KGTohI@yb?6u*zG!Y93w|u=9>x0TaaBq1^nc-s3_XU?sHuie>%&A48f0|rL6SxhEO@F~VJ)r1j4JorIh$wj|z zu({o#c2cJI#t^V1Eb@r8*=T&JRHaXpbM9{Cb;G@cPg`D^obJz)(-ctb11QC*pP)?J z7W~i!n>szIEZd5Kw6Ilgfxpqp$iqoG8Li>t78%N@-(piPO%BYzs>bMWaMtaSVj#dS z8#*L>m}8?9b5-TKxfC`@b#r0v!>5h%Ey1noKEJ{jRd86i{A~Yvqr&zVBZkULsvn+N z&PhUP#QJ2891v3g6&Bx8cx>g?_>N{?@pPkd#-G}!AXIs_*qRmqagiT{x))v67kx(r*j)4PoM=SW=jz8D2o&G|ZD)O7r76EjQKaI@XW6G_<>c6* zK0w>US{CE2(*(Ai502LznZBU)kM1o*80-{0-*KJhJ%7>L#}5fEi0aJQw7wX;`deBQp(%dYT3 zWI(N6&h+JmA#au(+k71hE>jVeJuvggR#K_STkvm|8Zce*_~2~V`R?-VtMB1~VS+XH zKMWh3P-Xe>$P{bp-CJ zkd3L5GL^rqj|8Tv>XUV1(>`6IcAzI<4!SG|=FVf4sc|rdtpoMK7To2iD1B8DE zNT8`NLLYMdi2Q`+v*CSVQuA)dYdd4m5x((xP=+*Ew={XH&C@M|)#-nD(gz#ZqI#KG zX^Yztdp9eMGZ3_aHwBzD+fPWiko`xs8n^moS7=%!?njKJgU8=p(}ez>4a#l-PGWp` zbcoHHvlC)ptM$zqd+(PGc;nqiAzl(ow+1o(62ZC)EKLkit49V78Xwq%_``;-yhy4& z>`LU(c9sjj@U!guqqnCeB#82xum+Evu{5Afc93D50l{g17DC?}JV+n*6s0eEG$T;j zeuo5T=*0Ak3RE~`(>!ewv3b?EefrPknQN`P7J+NhV0^N_Z~O_|^@1@rbT<^YHoSdr zxoq+v-ML>ev{mXC_%+LWGao>j#PEI?x%rPJI@I&#{rPB*Gdy0ae_t9D z4a`lB6iRw(8i)a&t)3uqEw8paBY=u2=A_T}rZKHA$9Hq=mwQ>EEs8Ii8zm=QbVY7{ zrPX>G^#^}%*?x!lgH_Hrv!k9jTIYT%jo+cg{pzBdufNA8&)?d3nX;0fwuJU~s8&MN zy{?-jUa6ewK@1Mo;{q1v34o5Ig8{Dp2AEPtmX$UO6y(rXbcaEM5~*qQ3K%JULPE{` zinmIE`1;yVeOre7Zb-eC_0R7iACLHDv=o0Y(7V#^Juh<5>}b4BRFYjz@$wq{ioTq} zVfyrq0{buQB}3@%Jsa>(Sjm}0Z8$K;5RN!lnaxYi3+ zeD_$R?oi0nu5gdHRd2=K&NW;w^a|tj+t2<^-CAX5I3AzbhXfuaL3*cWb3*3eR(|q^ z?pupVu4yF~aR#p8t28BdS+2z^ew)kDN-q^2v|6d}jt@ySdjt&Tb*matG2;yiriM3( zL@6x+cETp7aMpBxh4~DJhQMbXr0Ar##+en$f~_%8(YErZ$ue*21C(s&WFx1jCbG0V z_9G;Gvg071dC5ZN(y`nD3(>el+O`Sb3V_GsTJ0MuSRYFU_4qH&_TG6a4s;$6g$AgeT=5+xp3a!u&g z0^N+$Pf3(|qKLMp*E20cr01JM@p*5QWKv)H@J=v8(;HZKX8n$$`L?8G`LJ%6l<1D( z1%;65TYRAbZ+@>CF?ao5xmLoo@bZ}LJLEw6+~sB3oi-xb3}LnMkJZ7OE2_i%!P&@J zWUN11eZxWIlB6J0Jx0g5Z`*%cDY^KGkt!=b%Gvl( zq<*BdfNqX%ImHW-ZOblPyS`P_P5X2ug4C{x9b}e=LH<2gd$5mw&y9q|iu#`jTS z+bhKIGnN#g!*)Ojli+=Yi_L=4eE7Sy)J?=SQ_i8L?=GO)m4SeJUBGBzg126nAK5qK z>yw#9nN&faAa0@Nfuasb{3(KGZ7F+Q9Dk9};F0i~HugqaH1ob~iN7k9 zj@gt4GufY8hjYf3LEU7qmw28|?l+P;Nm=S#zD=dUR@qtDd9+H2xbizQ<^l0QXE+T18_( zvo8?FH28BuT(c5x?cn?-WlAHm`lmq9Z!buAeP1U^F`ZT>5`R*yE)dWk>^b+Sp7S_z z9@oyEaBP*u@_QeW%k?IGD*Sz181ov)-;mmW z^>aSR_~7*dj4;!9G#Av($udbbpY$zxu3-)UMD#W@1vRKDOIutEy(mV`<`o}G`NLL9 zD!w>^hmBz@>qo+zWq3;i3D;bW5xaSHKEa}VUe_mhv=Shr3U#r+6K(cm^u|?u_N<#H zaTAjEbCavC`SW{JV70DlkOcbNiI2ZN*JZ!5oK0G<#e%LX!U z!AT8DE%kL8TT+f!^0Nyf+{$x!WkLS_Vu<(ae6w1C#$n;k;Ey6}Oxsr#zR(3cV5V&3 z-?89#(5ZOS@a3qaF175OiD)bNA=w6eqoJ$$Q}+%2yOk1|iXl5mM8Nx1Le>HSnr2V3W#qp%rv+qwo{G!QHYSrTz8CVxtnseP}{Zf!gxx3@_6jB>6ivs0Pp1Pc~YAHd8({cFoR6MtwT5tX{f4x5WSaLMN z7r#B-1AbV4+0GAN9vh{wAPk@XATR-l+g{>R#DW*bR)yfFoz6pJmj*puBlnMh;$J%| zbD?ZLl?CUypu|4sgV?af0Fr5$qT6SSG=jMeRSC3FTgT^V>r)QoKh6`WEg0&5Z;_4X z2%UZb`n==L>g4_{Gi*O~4)KeE3eIuxxxbl4|0+$^By*Ftys~r@edHr3X1FqIc6Iy0 z9f6w{XxWNgy1pCMD%4zgJth(J0X*6V3!rxyX6xu;nFEJRJxEVcE5QJG(*f$l%fHxTM5+9=mRu`U)x zNlP`XhacfS&Cc+j`)By1gxHb#Kt$c{q1RB)F(6P%O~?JjPor&+KaUK*#m=0ISg!u; zvKV#{;~L^Dt1op5IAF78%m@E`=>NURGb%i>V({7zM$FU zoAjB+l> z^v#X4pNKCvr2ow%92F}-0SMMGHq<5+O65b(k<|eyyZ{IfkQ)cvE=eB`?!qklYH_)|>t>+9{edUcebx(Bn< z+!NPz)UJ`n%Sh8~7lCn%E6${eE@13{%0WHvpTvs?-t@D~rld5C@&J%Vejphh)bIVj zM!Q7iA4hrh^b6yiu~|^}dc-9@6?2A*H^{F?@1!}qY~+i0R_g5W zde&pT1#5e{5#H8TJuD-YqY?o1aT2oW3F1laHGNb{M? zfLXxyk4YImwYTTalaXKP)|>_GVEmaoLGD=UA|?Lm1(G>|)pSkQS!FXY&`~=JLILm~ ztCMpJ!0E_%OIY9^@1wo__<#K)oUho5JKANuiasUs&r3T{k)=4**p{{2G!(VnQZMrU zgY*k&Z$I!OTjc_usVF}-;rQPd`9GKK)&ssne7p!d={@%ji1-&Pdl2*o({K*t|6~CG&1^9MfPIqBn)n#4%=2Z4#beA&ooN-G z=TO?g-9x&77-I@_zf%u2uwH+5+Ep&0bATi%AlwV=jq@ru04mli_ni(NB1CgA7H}~8 zj@0e8PN8?`A;@z)U=IYG0XEeeb$9_=h$)2tzvL*b4^Wqvv9ECUU107ZqKHTXoceqVw9x&_p2Qc+Ghn#i5vRzoBW*vDZ+lg0 z>z5c+?9wp+33is*3Z!u;Za+*s8f%joIR;#Cr%aPG{TI+glxG>>N7swBC(R&@I738I zY=2zb0n7C^#ibI{&W!qV z+K@b5GutHpH9X1g-~gaNkH}noWrdz$EaGgc7`6WE#J9MLOozSMu+qsG-)${ZCcg6Z zeKNWKrFc<)p07mUlh=y_m9dn{x+Q@gpah9NjS1+NLYqjFQGD9~DsIRd8qU(T)B z!w2W+pveaK`-1xbb1fff-#3-GKlG}elmQ*kiiYCE9EJ?kGlD? zeJ-+U4`|=-$*EMo)LmbQFMN}U zl0&%aR*@UDC5?!ZU*V7Z)#Kz*wjAkMXAxWD{k&slfkd4@0u2TL)A~%e61qy4-84NS z?2Xb-4tHJ70Pkbu0f0c3*peB6zV1it$ozGH3g>h=V6Cz*^goG)bCS*rCk$EQJIu&I}j%69W0-=0*l| zo){;TF;u~%xwnr$NBO>H&gW}#Sdw(B3z(xcv0=_$Yuy)(l-?_eawK`e1m+1B;(uMF zI=J1mIKtH=v|RThL19(_T=V-9!Kl&u1a{Rp`QeTLunH1WegUJ&qu3rhCwGVp^od<2 zIRBu#eqdP~faF>i3_N2n&iL43a0fzryZCt^Ih=j>>*a@@)m_Gi!CNGReTBS*XFZQB z%6qJdZDeZhs;A8kxZNBcM0E0*oD`$+-+GVv={x$l1x=rz_*Yw8V1%CO_8D&Peu+xr zCcs8R+~`%2e+{eY>W8n2X9j)V$=fHd77o`7E)<9$Bi4)}CoTn%K(ow21YL?Ih5F>m z`jd?m(tY3X@Lv{5v%Ly1VNsL59b9lx5McQ?$u>VQdkVz0fOJ-v2r`9B=~ZWx|8Ba6 z*92(|E*0!;vk|Kl@qleZv1g>z!mU0V5;vN#4p(wItv?-d)Kv z++8aaPcl#F8Aa`tS5yv89WA54^5DXX7!GcXu&7IQk%{A^tsh0cSYQx|%vO(sViyoHMePsGKt_tetNY=99mPX`y|}bZch?(u z;}|95#f2dqO-BTZHvt(5oaG3buC~p#6><4ULGc2yn*O2J11MSh?=-n(ZA&dAL z`iC!IaO)(q^EpbCH1B{X8Jt0#ni=i=0FRZ2mR=R4y~k1BfN80A34p}dpiVZN!6Zkz zNwe93XhM#n^bcp+B1xQiy+7bUimn4t-A?bxDBkm`a=P@`L(0J9rolUh^9=Q4>wxNg z174K%jfy9i004ErDTx+@-{}1aT@KcD8MV*s^n#1!I#_D)gPV*9b2EO z?dA%pEZLBslsD!2WAU;&DR^6d*rI#a>r!^&uk`1(djMxgAJ1Nvwb=uF+N=QWkR4cn z_zfsPPbUL39shjw6Tt9e^zK*593cG9*lc>%9}$9=Pd^6ie_RH(K7VE^7E#R;6jrHV z?%z~$spt|rzu6AHp9b(NQz9Lp4I(hQSJHU>Anl08yIp?5Osb2FPUAjuF`g(@NEImh zwJ$S62xf70F4HXU>h%oIYJZM8^MtQxl1D2<%(G_jOES>3KoM)ClJeY>oCTv+mF2HE z3z}1=?PY4?mTMfUjL}Q*4O33Mgcs~G30c21TAHxC#M&=!K)ue%Ti(9meqNL zZIaD(I^J&DdnD!8A2)#Zfb<0W!}#GNU=NsKA-Oupds!y$?8~0vWjAQhK;|+NS2+RM z84x)!k2~&(WoL?yVZQ~yr8Sn8ezn!0Jy~zHFJAbwLjEsjp#8jGLl?K{o*uRZ9Jis* zZvl=!6|4y&5uXdzE8F5jz|><(3hCP-y-BTdk!$AS zWrx)dPJ&#r`y~FQ5Z|tEnueb%;0%AC8tD$BK-(4xsHLZPSQXdcxEi8M<~1OYn=CgE;kgA?^f z%}V{d)I#nx-I-osBd!p)zt_XR#csO6YejF(+W;w^QQVTX3HP~Z%aOMACpXc?m77@q zE)AMM8n0g$l1br}nQ9eGf!VFn3(i=pl)-yD3q`K_I`4HnqV~WWw8e{QKJi$RQe}mo zQ&A!F&RpE@@J991jZ#@A1cbdDP_+pDX0&c$TQcH22sd(hF#CYLe~f@KsCuMu~AH;Q*Vrs7`6QvX438! z?ALZzrfaY59_Ddmc^xD?<|NtOUTAMAhtT1fq0uFlfiMd93DKIV9!eVx#5i~sL2Mrl%8-{5x{7hwV?Wum0zjzpP zRL{aA9(B_qEelN&xKAFTwJ;WI$5VOM#u!7lf^OlPHK_;wXHS+>nh)X#9Wwgie&TkM zZoZc6S(TNkP3eUP=?Rr|GJP}rnTu`Aw<0}cmPj$3l-r>5^=by{|cCfuZ-{3^YYZ}Bfb9+>C#1a6 zM|zZpgg~F%Cv<4~mr^fKC(Aa}^2ls|>H%JJlEv1D2YQrtJCsM@x;bjj(S=NqpRMzw zX@K`y>J4~zp``#@At$!O{*Pi%Q|<^hV6?y-uX(ns>E|QJhfUl-0~1IS@IJ3rkqHi- zG<+)NXG68(k8v>x?wGQ_&X}OO=?*v;7jS5{F8VJH*S7C@R0TX_q1-R1#e)2MvP=N zGYM%v1bwqz!>Rq^ZR;Q%H7eR#NKU`FjdZG{l)ki`xa^;8-Cvg?VD+27IR?>yDXuN? zBFR#FX%be16Oq{6ADezr+GS90Rf2K9CeW8X-yfg*?X^|VxxY|y?#+Ws# zKIf1ITOsM(G~`_xYcuaECHA2R1LIt4Z>Pv*s%9!ICa*Y~>V zQ0nI@oF4R2jV}+;h%44m6SqT+BYS}UX%uqFGi}I&WL}m6?1S0VlGY}1xL&E%^CDgM z<+|(O_6EURB>M~*z(fKDir8#If$`a|nd$aXXhLD7oW3wV+i8Dd6g48bDVUaL+oX^? zquN2(1bD9ZKwvMmeN+%Y8okBl{9O6*eKVXk8pw)>tUsi_r5I_J(sXUZy@lWG;rxlU z9!SM6^n?Q!fJH1u;YGrQZj_5)l;1sG^>drTS{h)w;pbp}J!&}F>b+r%_7iS#qFdq6 z`9?K7$tv^!NQ9;lAA!y6K60vqA^`}Y+^o75nJKg=`Pz5~@R zl!DJhT@Hsvb_^ArL=v=SVwW}nv@i3q&7HY}-KkVRne~s{1ffK%?&c>3?PVVg(4r)R zu(R>yEMfr*5e)bZPu4tam@tZU3rNG48M>XpeFpu{BaG^0j>h}M#BrD6CuGdhNq^P` z(qt2Zbq7no&jgnM*v#t693vl{vkX+u1+QCfA_QD9ZbRVpvht>_o3CdjN1Nu_j)T1P zb-(2v0=I1UJ+7uyi=1c(O6BXtv zV<`*w>7HUP06HacbcXgToKPNvw4{^d*#957~3oAiM$c^G`_PJj>OC@ zYmRA726SGW@|@Y598D}kh&6gL$uCQ(DL6I&(Phtjw%xDaS=R9W^{Z6M~0;SCy(N7qLO3GwRa3L(R~9 zbKyJQOq^X9k|@;M=Y3nyO}ZM`NEu?dk|DFQ&DREn&T{|T$kv4Zzg}Rdxf~P zI1nFjjy`cryC;Ng*&M|hWtPG5fFjs%Uh;0U7F={7n?w4Hu25<08;fMqI~szp?hamb zYjiR-`mr&cT6;zI;v*#*yK#7vN^d3I1EnT+CVRlK?Wp$;H_OwZu!GZM)q9WRGUOt{ z4cZ3m)!7D%5iWX_8G!-WN$Br8pl=^Z$0la-x*ELNA#|;I{xeSHg2%fOaYS(ZkYny7 zR$&o#ICO&|)Y6b%P${R%BzHp_ysY2nJtDCBO*pWD+?4)B#{rC3JA-6M&Cl-h#;BWc5Z?pL!Ti=s*n@eYP$EEE^5nAd`j^W{f)1IywZuwQROdI$4$c(;4&2s@qD z-fHL4*JFobrxygmHSOKlwKKZhf8?jxTe4e`T4VeCQwyM+nt|BUabxxDj@FCF z9qGcc38{G$$Ci&4gOatgNwY-EE7&BQNAA_yh%_(GFnp{`Kd+yMF*bz>8Fi&WbDD-<8}&7MJTHq01{ zVy+hL%<^&Rm_%8f?C($CU{1=f=k*Hhn>+O81@d02&jdL#oIs)!Sb^c_;b|z#o`m#5_#bnby&dJ> zfzu!{eJH{NNSY^1wN{t-tSCk!boAECM#xW-GHB4$%Hpr2w^Qs1(-c zs?rarRpshq%7PZ$aXO81-pOOhB7Di`&>u22uRDHiY z?AuH|BR(6#c%!k4csF0~rCPE}j=^ywmpp z#@&vo9@YF>f;!D?tE)yE#^WG`ZHKsue0iia-Z&d}Yg}>3x#&O;#4%>Xr`oX%VIN#N z@sCpVX6^8fe;!bM)&rFaNWr}NaulmAn}O)ti(5S`s3iWIJ$Ch?*fB6UT+|ffXE9x- zjQYteX4rj08Ds;MR}JRsbSZ%rX;J88j8^XYM#r$J4IE8%nVjx?%Zp)qJCNQ7r04r$ z`!BgQ()FIGXKp`=(DgSMYXIsO6NLd*zKfjf@FN&N*F|JIt_Fv79yPI*_1ZS`bDa-& z(3uX2RX_%nmHOKGFf0NW;Nlf@w>&o(MwlvVj72-E3eI*ty)2xn~j zb}9lDsS3SpiMz>{M%T}T>+Bl!D9HR*`_bZVUy+&M;<%SF_e)n~b&erxp(^6vyQGf)EGB7F4)+}zM=CR?{pb+&qYUCpUxS0-m3n%e){!Z zo9!n34%mvU5^5uGe(3Py<>@7;31}F4x9#zh_q^li;|Do17@fAei7EBZ+%&8Y{fXFV z0x|io%fAT!3Zi+Dt@$W!G=#1doxm` zm4}d>`?(pdXPc^ekN;cP!29P+-ijBucVzeJ*HtuGpvqfwz;Rs}pFCQZwb^~gbF(C>lSmt>}^fg6RMNNm1B_wFuSp)F1{WLfW2HfeP! zQ_w(ngjX7t(ICP_-C#?l_Vp`U?~kj(8Ryg4%<~}2kNW8ClwC^Wf*1y4O-Hf?ZGB;+ z`n1@x)jf2aXr1)B8IAqNF9q2fGMX0Nz|MuSEmjK=G0ULa@9;NPQqJL*7vXUjDf0Z3 z%16PnY!VrkEw#k!c)RGdx5X{e@o{WVNO0PvW-gtRni!;{Jh4^_N4xkle@ZiS3*x?RQS zcfpow5+4{pv4ZZG@uwS)bqDMiOowSL=VGVwRiAiG$JE@3toDe1ZoYQ9o+n{C_ytt7 z5G~;%82Gju{#NdZ2z15r`4!M0-2HxEnFjO<2)Cr)j#AUKqqw^yQCSj4F1T~yJvp}y z-FMdO!rdjW$kHj=J)?@uxN~Lc+xfYAt1GOI#juF4C@(YYxa;~LXLgOY9EFK1^ zX1tcv1nPi$-Upf^oTCYe%U()fZj`CjGO3JL2a38_HahqSaHoBiYhY%Ejbq-`=w!@^`0F%mugMUy8m^pJ+bcBQo&g48REt^_4K7G zSF%6w2HR91-FN(}FD+isc-~t3Df-J2)h(N!F?XBV^+e?f9fet(HqloHjg`q7O?A3b zq!!y+RkK}HTNkuUr+uFbSxs&4!v77WmNxr%I#wSxqZGxKnE^%tuk=l5^VQq4W!j~? z9@ceN6?TzV+B;K=->ksNCG+%jS6`d>HNkW&CuYv?C_ZsSu6)yf1>BcIjC?d7QlTaG zt(W}mrHx9_XcW#5XHQ4>#MeMM|y$EzN#0Kt3!8_@n&>qMS?bQUKjY=xnGFq2&yvX3BG?fZGbzJ zTD4C3^@KOK61$LHXTmnPW%SOoS}>sooW>KR9iS)OZJtzrHH+(Rj74?aVc8DEINRew zCa`B7s?u7Gxh5-cZjhX5Pf9aClC^yVveVD2T zTWpWE=W0&$<~CR(s9vySUAwi591j*wwWxR)_+2g#&T_e_F$hW%IeqeA3+E^)^g1<=gw`t%PByzC+Cq& z;gj*Ys%hvY*H{v6ZDiCyx4L4vgl$a>`wHgSxw96n3>n2dZDe}??m?0m*O5e<1#Cz4 z)=&uA{}DcIigQS=Vdpfbjr=hkIWJe6Iata>hA)cn;8!)9^dbk1ZILBKS6s2dCP=~2 z!T@dOqsAI7!PlllF=eU0QM0&zuc?0d#I?5vb_xr~<|%f4H$icKiH(B`^K8WNc|YIa z-5iPhMSuP_yI`osEodRN;mFs%n@6c^yV6Z)O}|I$s9c2Sn2@x_m@w4roy#%yY$9WoUo(q+b2IQr!Sv8&#Fa1 z{(A1_*8nA{Mtg>@5ln~a|H%H*h2N3w73r&rz(R}U3CAa>N+%^8(R*4+i;4YET@P*8 zzOx4H@yx7@u8s@Zqcc~@mG`qvWZ{{LQ5eg-PpIg%uFo=)mRG%M*KFMGuTS+}bS1;{ z!!aNaL67XM#6m82MSI)6(Zoek1n}$9u-dAvA>d&p0+c2B%+bL2S}krf-73v?si$5W zUE~Aqlo*zuk9gI_!cO5faz0xQPen;TgwJ4O1c!k6yPqc{Sx7Ym)0M6UQ+B>__g1@| zwJB~Rh#wgoiP$0>oglIc)lEg`+Q@OGthQUdGKreJ5j7{v8whL@lYi~tNEdbwl`z+g z(XKhVPraHOR@#$mXrUD>60W}bjE5t~h<@-<+HH;C@~)Z;#i;?xm#kzR{z_x%f>2uL z)CGS0$Wa-h9^&LJ(_3DsX2>S8>mm;B^r48OrgR1>Y7wsYn^<@jW4<{8d5=;K@3)K~ zyWovJkKmX4X>>6D$utQZ;J}Pn(rs)RuVayQ-^eXS&k$T6gy+c=0*R-u@X9GSbgUR( z?5oN8cJ-1$mnfyT(YiZK1{-stLzI$AvwV$kCF&UX_acYHDdJni&Q$aZ)kHQIa{LU= zwygAxjWtJcwb%vW(Y5Mb^T~pMh4_WCA-QTeNUrWt9Q)$k{kWs_pbYv}J{-os<~Wh7 zO^dbSKEZ1|7=~<{)^rz7)3zQ70LSB4_Y(a~$dHybL1hX8JW#t}B{bl%GcjHR$e>b&K2sR&`_3iGF|10^>c`{0d#2WY#Sn!XM0)$5@hSC56yYos(+4Osy zs6)K4292=gL?%q_f)X!&yeO#>qF|uwZPf06uS>tXElB<n~1*2 zQ!-6yV8sgQaZ|WxSSp>)bP4MS$?Y4IS7jCZ(g(}(ESzxe7p{pYf1j1dT&`-d_|>+} z*h<=XZ6aQK;Fpw!^40=CZ?f48I6JCD8fR)KN1LHI>ReIoLRlRWf*Eg!FJV+aic+{{ zKjU(mep19|F0vF{h$WOjE^pT$g)%F%gSb?3qtbRD^=+`4R6~k$SML*CWo(i+kGRh1 z)Te!$#vXk>EIT~-#Df?kDv}AIen&VBO#y93M^ZPp_m8$Ae9;~T8WlsaG}872w)evk zGpM7>3d@UnZ{{?;x@P$X!(3UIlilSiw9dHh-}b180SJ#6NuGy2RT$Gkd2v~`z2cAM z$V)u^Nngi&<-KAj5SwoFx`K0yo7M|;y(NPMhHHmE3j+oxy^_R`ZxRqHSVSn+y|6IA zT~nXE(xCFtXa&LZegr9Lj11qDTG%{-q1N%655dTdk#7yK!$a@2xS;JCu2$D>n%iuE zB#7`NYg+5Jcnrq_hH(zo40rfKXZlN}Zeo9v% z<*3@#-(s=}_oev--=uVF{`Kk!<9YB>dj7*kMy!S%Nq42|0$9#Ce%#_i3U|j7Y&Cse zO`6918b|>;(aY4%Uh)A#f;SIXYI8Abw;v;oy;0G(o%g&cHnTBbsm>n#u%2+}1H=)d!9U#5JVw*hKkgn! z+XF0Bmfy^!6}9L*0+cKj>I_*G$Q|0w#AG-a!lc*sdLQeL=hgcdo-AzftkWR|M)$$ZG1Qy0G@6}hnIFSO; z#DfD=B8P|Y_>;+c-?NM&&I82m`aXhKm)$YrRBhc6HM+)r>2>lHZjn)HEhl|P`>!X6 z18>}U0F|o)z+YqK%698NP)2ycY(z^l5R%4*!=mE|;U+W~QzR*b$3GbEF1lo?t z^qsv1aYYzXuKv#Bx8`M2b(TLU+~vmudP*maxlkA@)-v<6U<8Pd_g*Z4@5oo?=;6^% z=AHkKy|;{ua%$p+Qc6fm3(|tX(1OwkDm^raGzbhsi4LJiY-(skT1jabLQ*6~ zX{n*RVLp`7vPIW4J3(h0Ta(p1L`?%``FL;VF>>{& z+oMj%<^CoY)~mCJ*0A7GX7;dd*2y8Rzsp7Mz5VkY<%fae-N%aMVr_~(e6eZl^RD2} z&<>YYNoaL1v4qu!Lh_5WS1poNj>S@sMPK4$M;~|SjWcNF;X`VW8VIQ z(1R2@ZMI;fxv;z*Uj{2{v}G)`!PPj)^>KE6V9}Px!=)Up&-`UUJ{u%1&JzBbRjoFNLWT*l_8s z8Tw5KZ-sC`WaV~esqY!ZoEgn=g4I9#T=fWJkX@%l60*&e+ku-p*u$Hzs9m*B#I+}s ztdY!>j4DCzemeV~M5+?ZQ5*bvo3HHw{R7o=ri9!?AMR~rGC1U6 zp4xorhQ|or5Joa-E%lhP&D$Gkc@)M*YCf>v4R!Q3H;J%k`%RYo@w+`!Ik3{k`+rOx zVBZ_pu4m+GEJGEwIgk-|triArw2Z>7hvB--2k=LJrq7u&XC##4ico_-s+EOx?8KKI zy3Nz?xe-pX;YZ78$}wlMqV6+t&mM2ZiHl1T4!F5>PurdPoy_}nS5qQ2q46H(o}JE` z#v~LnpfTU^n~;)5@M61-ROUD)SEo&kae3Je^u#d+u{w$6%~119cB+cLgvN3>tCNgm z&7S04e$93-iAh5gfpnv@_Ty_S?`xaA4J0;YQPxv8S!b{~o)-(vWi;;tZNwj2v|Ku~ z8E*7i4}Gm>;O^~T2F0&j@4?D&P+Ma(Wt(|uEa8%RAC6`yMfIWGG#s5jV+aU0`u)<` zRSh&mAA52y9wTO#d}6EL)pduJ6jX_@Sr`Z2?uKLoIHubryUFbHi&S+R0pr$$800ve zmt*&IJ8L0}Ub3Cv-nAriKEI&a?d0_&EK7rP_oAoWTfM(2fu-O&EJJ!hVM+Y`{VmY_ z<}wmxuy47~-=P|Hx^Eu5-mt$TJ>jViq4w zCEASU2km)&4T1J88|7dQvhFYHq2Z4X`D&e4KyyiLdk?`L=xrzG7~sy*AV zuV~OCR*QW(b`08x$CrIIJ|yE>MZ6d@&vG`YyLFg}dmwA+3ULDSL%Wh7CGW?CQ z%#th#{7asXPN5$?zEV*g{sehWzBKoCpJc1WL657R6ywv;!O7Y@*8v+{r7>ei19~YM z5saaf%Dp_qGp$qK6?4XAV&_D-1lEh*jI*PzH+ijZCdC{_X#B>%UR68QcG1SkoQJTk zuZeE1GIu#2k*7t~ojr3+Z;jA3dB~n!RXmUNvA(f!s~8jq0fG%wS*uvtIe9QhbKD)^ zyWNGYRZ8l0H44g1sN1D`aB5|w7*G`Z#HR+Mz5X8IRsC^iZ6~*l{OxDbT-;RE-t?}U zk#&|%0i8!?AJ)pVP)d}%Tmx;9+1GJneYQBoT5DeW*k>A|f%b9JSXjJO%J9xplUh6l zhaNKG_aZE+`lPEmngQCTR~RFx27i*jjK&#AwSxI?e(B0#A%@!&D?Dsi9nhD7ORG$C zH{C;$pB`M`oIS3Bs4s~}F0TQO*nS9ZSOZ<_D=pYAsXq38;|WB4&dgr_r9N7QeV9I{6ENF7Rwl1mfvTpl>u2=YcTIHn?DEg63N*2$Kw~czblm&3Rkk8 z*($H7%~uN78H;3K!i{Y0Tnp+QIsz@7ZUYv7O!PG6vy~!SlYG;HPIdhb(nb_a(_=7m){>AXr}uU<6uhZJ?GR7TaDXFmfNryzHD%e4(0p8Q@JRsS+ExtY| zxyt%|uQ&Civ{SseO{n{}`9Q7w^JfApvD7h>T)oqp@YX(U%Zy8LFY`$YEk=`B|AaF_M7OVO6K|YoJey~(-_2~YPPfQh%V?U zFU={P-(+`IM`EEi;ck4!p#5{+Z$Q*^<6yWfvYgq?Y|P8P`&8BxWoPVjQ?1bPSQ&ol zCe{X`?33c;wB}WlNgGk%B|-o6HE+v)pAovMjzN?t`Psw8d|^M8-Am8=v5spkOVbVo z%VmbW*%g!U+r#)9o+;tY>hN_rQ)Welnn2>Kvso&Yn~P;?IUeHE9%iqjK2b@ zZUsSyZcNB`T&Ir3<8H))a>SG41FTx0xSCSBJKR&5i77OPW{d5kfAx&t0!PvyUuk5+ zBlw+r=^k?`x;JaXGxsWqxR91vAJf)CIf#pJJ^9Opx>wN@hJoDoP95>Y@tWE zfgK^)RBtj~3`oOA_n2&gK0p-xZ#OU!4oVGzhsF3ear8~)$RGq?Z$tcXGK1_=kyWhG z1){d&0wqXb%d(~K2p2Km6{1^q-(5aqJA3|w9w5`3unY7(^DKYDMvhC z@aYDMlFO-nU;YxGLPBBK?wRyVZ_M@nBknfmMpBwa_gk?g$T4ZpbOG(v{}^SI6K@Y@oksx#o|})-Ilj`9^KpIw{NV2*ld6s`{^9R z#s??=@;H3wNvKd zsZK5hrkkD4u|5%L&J=f)-te`#j8Q;EWwc^%<+c&&Bu%I9uttkR+EnSyI)!Z}&{(0t z_kE{?d$+3O|XRx`wJf-p7rbxrP{w zzRhpznRZ?pGpt!=?47Ce)^v2+uftzh`T`wS@zIB`bS_tCoUH!Vu6-IUV=QU!Db)^K ztu{GESBAL@vWsrQZ1bjg4`>|9qwVg(UxfOtDKUU?r3$R;$z12L`w|AfUs>t6l)PZf z(r0cf&YbCNHNKR#aVBW4*lA9lzUgMVl#eo&+rfG?3rjzzx%I$95?wT}2MwziMEESt z#|_phTu!XVSVvWc&5SBhqeio5^vTovL>FHQU{y~jd1Vj!nvUUbRx0YoS$=*MGs)tT zc?=TP4FxcffmkpfTBCIPJ#CV*xq*$-a)A>b87|%!VH$kX>MzWhSaZ*2HtBMyH5Uk8z&_eOB=gI2ggnZ(3&nvlTdE#iL3DMzCw<-_V7ow z<_ZtH(XI(PUSsU3F?}9$tbC0fG^Bgny3mc_RZ)^RKf&7WaP<;-byjmc)+^_$HDI-f z^BapCmm|NiGH77z0TO`8%tRDKoe}Rc=ktUSY|YvvT{?vt@N(F2Z_1 zt}8vh&D;;q`M9OGCvbKXrz6z@y*iH{;rX(?G0;ea3$@92S1H8;w=BvKv(GDm)m(aa z^q7N9c7VM4Mn|Wl&8`=o?krvBEr-nLK66mkd+Pc}$8fny4#(+13|%-QnAbh@>GLWy zCUw;1?vO zRX)>R*^gg+mj`_u$p?QwI>YRyidVB~m2-;aEb_|i7QKZTrfkGExM9O#hV28js69a=IEzY?+E|o^#{n2WtosIH=aDZ; zdnE^MaGs76W!SrD1lNVtD8a5bZ99q)4U@7L==%b19;i`;=NVr!r+B4j!zPc9OHbO3 zACB)`wx9Vdkd&4|E=6}sc)?7f@JfmB9S+vS)@tE*xPqf&P^!uq;Wk%gnkPJHonFl_ z!Yh4}n9d4Ol%2;?TlJE61z#S?}?{H z;S&iO*SkurXBG>lJ$HWYb6jU$1sKWV_=nz@sL4hDkIC%7JFb{}^zC)eZjWlu3~L)w^nETX#kVNQL;U7a5bXr@*cU^(7FdYUdbK-03JN!m zgf3I2M>81~P!KX;xxZ=5dQ)F9ToW~hV^!a9OfEVoIkCCjDcEkp%N^UN+^V*dFE~3p zw9Ms!$w<)D)VE1@-JR1wK`zibm^l}}Z5%p|A z$6d&JpAX?%6`!B*RXNN-d9fx-m8Ku70ar&o@x<&2lYfK32s|Sq@6uEU3*0FBK`IH zJLuD+Zqh)TA&n-jYdQ1UJ%`%QxfW^IYM)qo(v;uofPO06m@nZ~5zv4We%^>6aErs-<9O(i}M>z($zu#jUE+xyJ_k=vABG|TN%8QVCa z14mBGG+V82*hTSB!B3u12TX3VvGH&E<&a`a5I{Ar3J}a>#9}Wm0gQx8nEW7 zW05AhM;~`^<8;fzZ4FI+L*=+k*DvvAOF2)!2kSlKf_?#&FG73QsvhxK8`VD;S@-&B znxyF3dHl^s)6kM`d;(QfCfU&)xVAF9ftK<>HC-3}>4v))pS`RZ#6dqwI*d1H$~p4 z!gm#iZsI{+_zyl4U&IAKQigh87=hs7&#_0C77O;-ap7`o+qh_zey$XSY$M{@%yr&FNt1DUD3ZoEBVh>5tC~C0ky%oVO;S2niAIk9@;;;5h!Yhn~yD}z^poW*y8Bp z=QyPIm0SUAgRsO}rFjW>8DZPO6XV!@06glv4d6lCb{f@Wf7pn>NtAviJTiV|kv<|! zdj|w9`-B-fC=SRZVHd7^V(9+Oa#e01-(b)X;2qe5zai)J2dfA2NXPHrrSHde_NxVE z{mq2ck6)gp0gy&}E70}=XYtGlaIgjT)O@w0lDH{G2Y7Fyh)&IL|F9Te<;3$jFsyA0 z&>9$pQ^8zw0zeISe$A%7$6Eirg)adRo#ZHdUv>1W;iQ%ZU5bhLtlBtmVg(50#^yGbh0ylK?RTnPAXRn>(`uSGwiA;|If8pwyGTtHpamoVS+2Vv&fi= z$j=wJ<=f^r_kU3T$$S6TZin!^xrHeP3obYJt%vKr4hPDF|G}FAgC)WX zwZLAt%l~r4;4{CM9>2uZWerc065tTh(dy z^BVRK9|*oQ{~1%tIsY^M583#8osC?B;>Xw4Aic|DYeJJX_3?{c4g$*0VTA`}Oyg#{ z`oI0+(Es&MCd0s=&9V<4f#SW_oJdH_w+TvAEA*%sfk#=DluZ+hNg&l~*XC>!ldbCJ zIjD?8kl$2Q{)usYL)Pp{3eeJ~@O#?EFpjr`W>!=SJqQ61A7vs>yZBnwarK;i{c%U6 zHq9 ztAQyuA_F-TT|xi)+Qx+V2_tx|!QfGoDhVtxPw!yEsJ3`9VEwbu*#?Y}9c*~G;QI|;LzMCb49^~1MD49N!iUk*H8eLcE zIUi3IDpY0jci#FBFR=p-3w{OMYkp(@_&g)m!+2(EoRp5(_kfl#x1Ae%%OSk>Qf}QX zU2W*946%KbNXwP_On$wMsUb|KXWb`6j_RJd^83QRhA|DbBUsf_T_pd4rA3; ztHb@9EC-=h9C?kzRegQ^QojXEt7l&f_ z4VMp{7_+GF>Je20ha2i#ILwZR&(0R%G>!v{oJ(@;w%PJ#i$<)uG8T1-j1Y|HBj^P! zmm^HZad3JmRGw1#3Z#zud#F`*{=fziA-x(2hY|;?Pv6uM#;gL9Yp1IFzb+eztX}Y| z%UH#(eUDFBSFd-n9R2=U)`9$Q4v(M1f5+WpmfN@rj1Bvo#c`jsr`{t9VgYIM1S8)Mx0x49HFRe6v-e{eY@@C8*v--c7co&^?cri|-&r zQTc~Cl5ngj__lbF2I1}&tzzBifq|TEnObYa5n_@zslalJ=kwTpSmnPZp67uH$RyT6 zH+n9iao}nhdlLUzY#d5v(Vo{7b+cmhgVJp&*$l&Q1c z?HQHH;Ca#{?Bd{vy;NhFBasJ5IfFK0DVL^h234=K7Iot3q|lhT55_xNQ=n?5JEsQ{ z^TMF(jqaO3>&Gc)Tk*gQ5z5uA=e}hlx zO?B(Zaq)rfh5_FBbEi<*c2^uWT~Ep~GqVh3H!n z6EqCHlO(DpIZZgFHf!pbo_&Afx5UC^!tpM|I!=`LV};D!3L}}VOA2{WJ0Ipc=AnXM z?Unm8o%QiY$#Nj$dWs(RY*5jP_NsB$i?wWj&Lnm(Hc3dO`@Uf06(T3B_Jf%OexujH z!Ed3YtQW5lWb4ME0c|OT6R$p&+wdxiV+D6r5#Cm&;jEt~P-Q3g$ z>oGe2qKo@4>g)w2JS9Y%#UUtJIZ_X&*qwi*|Hq?&6lYP>lgX5mWGR~fe780^a3g%$ z^wq|&(lF{!{+aDFcO3k1o4dG8Pe!>h#W9z)#P0pz!OQRFxcp6JOtK=QYOu=VYa(2P z>moM;l9T;$8q>2NdiGrb$w{|1u`)_>92wjfBYXmFiRsrutlE3i@V~`YwQk0Ik8&Ndno&fH@-fu-bgV)upu5y_w^QCGo|B{eBN8V(u zYCbH*aU75nr^Geny=dzCW0#oxO;yhDxglom+>zey6sj3rloq14lxf2)2L5MFX9-K61IcnH+-!YK zLlN>xK$!69MgOO2Uwp4lxqa3vNg`jkj)xy-l+Ic?Qp$>ASy>@BS9aJC|B@1~OOA;_ zcp#E}CX#dMb(CqbkeA(MC`?fAmi^yMsj z5!uh$%+QhNjcg zinzUV-OOjRby#JM7`A;rKR5jkio_qU))#I3 zjV$@cHA+$J0fZFnI}gV4|A*4>-WzQ~*01*^5HOaz>LFRku&=qVc|07+?ycS`!#EVw z%Ap*jY9tLSGF9*@v}36$q#lq@ntGvy%x^DMrFjfWRnN>g7|C_VB0OMq@~lHI1D~JP zY31ml#*H3blsF`5@koYUyIJz*ar|V3M6P(|ZDFBobke>@{Cv4B|Du+>$3%9IVN>yG z4ok`d5@|Lj|2aa=X4{l_FYvr=r=Va`<@Zk0-TSA6tJUgCM}&u`9)G;4Jg9+^?c<1ZNEo5e)n*5kSjbG55qWd1_`Rv(tJVy9yYPy} zp)`uKm!r34fT|v-07^1)u;#t;_&PLE@C)6T@vj9+k?=K8ZtTgoofpzig!Qd@|0J`- zDXcd-U`Lkwg)jlwaj&)elOvb9u2|(vjmJb3Q6ZZxR*Q-=2})>VQDgR6Tkr}cu9+2x zkh|321${`8AI2{~?s*Firj*YhGI*9i7Lz#0TgQEW>TW?+v zbV{EdnPU5ONW4YN_)r^i6YGE@B8Atp#Pj!$EmX#I+ljnFI`>TC$Te}=@O8_RumNHz*Gk>1}a{Nf|e|DFD2A@*b#6M60c7?Lo=s^B; z5NDw@9_oQ}md&V7;uf++4!Ty6k{$Af82ed%(?s+c76EhmGS7*9c)eXJpimbp_ z3n{{eL!gB>r48QAEILGevkp#K-*_h46x>aTslDy_r1AqRs^%z^Yci}N8$Z2Uq}NRK zU+lPlnpmK}|MN!~b(6Vx>&qcG%XQ;GZ)zimduZ5I<4AEp&jBFj5P$$r$x{o-ripAY zbnUtt3*x_bi|lF%S5kZr7B_}%3m>ZWb_QC;>apWGtVzsUUlX=5`gvCZDvpG2W2NWu zu-zB{)K8NFh@&@vv{LS_92J5w+PmL3!~N*EUBC=!5DwWa*_g|g$)A!`=5ZSawAGNi z(n`XgiVcSF)m5S(hLqI3q$lN(wWlYC&feRAl;NgoykMpl`Jj@CI3OU-lAn4tseY!= zJ)1{>okz&k^*Utfp;B;=b#0jnUai{c%{qge8L`6N)vA};O;=BMy*t+phE)q17~H_@6!;BX zH`pYY5D3HDd_`!)l>5tiU^DkY_6zb;pF9Pd4lk#6fOvfc+?4e=`Rh5F)X&jbG*hktp@Dc&h}i zFn{U`%)EDGP5^}eBR#3vS6DoHL zxHgq^;@BANv|r!UEd?c?viU*yf4Gi+2*8_u{0#*14h+Up19BWqU`CgG91QDnS|ot( zmL~Q@JMuXYQ8!p%0jUC=y2Pq5M~;Ed^=e2xct{kquJ|ozs~}tf*sI+T2vHk}ggQwn z0jCA^;RQ2B#zJrMohHY`ctZ`-X)}7x=4E%($b0$U^(P# zTn>IEqWS5 z$hQ+nJp|S64s*ZS+<=zPM5+V=6!g<&jO^HZ4noM@sOuzvhJUBKhXY`r1QZ~|N<(gd zlRPj=T|C!pSf9JF?qJDj%xy4$j74!r<&zHgIuxTQ?F4So!xkCpK>*q0RU9s3vY*a) z%n8OdFXPy>3#v8zM4f!2vejd6;g%gr3fuU2;r*`p4%Y=bCzUI~eiir$< z3SfEy?NCQ=lG?7HH2KiHNg!5oQy^T$J(nUA89vm9AX%ru|gG` z-_k=da zrS{9rKbX>goBsd#?E*d?BNz$jXvv$ex~Soa%TIj=BV7IE5mB>^m0w)(9YEH}C7ICe zog0I{D8^d|9wA5causCH&Qa<;e2YsaKG0vSzRdE>DXbn)lI3Go@yQqtZOI>BJ&Tx> z20R-b-^kXy!^Gw|{XAsx0<+49iB#}z6P(OFPj5P5q?6?77z~qBOV9^SC!CN}W;J<`RqiTdbot4k0`r z?lJMO?*gi!K;X=ZPDqAeFYy$~z`z~M8VM@x;3-kPg3z2RHg5|Q9fz^t7~v;@h9MSX z>7lgmj1!mya0B-m&J6*UG>YC5zBEWGu$yKs^*YSYvRf>;RyZ*>DhSDzH8AQVL(2*g zgl-RjX}^t6Ea3=T>zuP!%5Fi;+C{(yrE8da^1D=7D&V5$D`*`dGh-O&T_eRGhfRlC z7QzAJg#kp$=8?Wl<_4fA4*7I+|wl-Yphy?zBuog)XcQ;FALrHuk9}1soQ)((-`e!VIP2 zhFK?p;V+0A1Ga+-;NI8BBHi!dqQIHw&h%0Db<0{@DONMq&)!&_0e!9y4q?f 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�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�l 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

}oRMKbz>DtmFzHmS{? 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 z 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 z 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ν3&F!#V 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 literal 0 HcmV?d00001 diff --git a/examples/blog-articles/amazon-price-tracking/notebook.ipynb b/examples/blog-articles/amazon-price-tracking/notebook.ipynb new file mode 100644 index 00000000..cec05a05 --- /dev/null +++ b/examples/blog-articles/amazon-price-tracking/notebook.ipynb @@ -0,0 +1,1753 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to Build an Automated Amazon Price Tracking Tool in Python For Free\n", + "## That sends alerts to your phone and keeps price history" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What Shall We Build in This Tutorial?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is a lot to be said about the psychology of discounts. For example, buying a discounted item even though we don't need it isn't saving money at all. That's walking into the oldest trap sellers use to increase sales. However, there are legitimate cases where waiting for a price drop on items you actually need makes perfect sense.\n", + "\n", + "The challenge is that e-commerce websites run flash sales and temporary discounts constantly, but these deals often disappear as quickly as they appear. Missing these brief windows of opportunity can be frustrating.\n", + "\n", + "That's where automation comes in. In this guide, we'll build a Python application that monitors product prices across any e-commerce website and instantly notifies you when prices drop on items you're actually interested in. Here is a sneak peak of the app:\n", + "\n", + "![](images/sneak-peek.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The app looks pretty dull, doesn't it? Well, no worries because it is fully functional:\n", + "- It has a minimalistic UI to add or remove products from the tracker\n", + "- A simple dashboard to display price history for each product\n", + "- Controls for setting the price drop threshold in percentages\n", + "- A notification system that sends Discord alerts when a tracked item's price drops\n", + "- A scheduling system that updates the product prices on an interval you specify\n", + "- Runs for free for as long as you want\n", + "\n", + "Even though the title says \"Amazon price tracker\" (full disclosure: I was forced to write that for SEO purposes), the app will work for any e-commerce website you can imagine (except Ebay, for some reason). \n", + "\n", + "So, let's get started building this Amazon price tracker. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Toolstack We Will Use" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The app's code will be written fully in Python and its libraries:\n", + "\n", + "- [Streamlit](streamlit.io) for the UI\n", + "- [Firecrawl](firecrawl.dev) for AI-based scraping of e-commerce websites\n", + "- [SQLAlchemy](https://www.sqlalchemy.org/) for database management\n", + "\n", + "Apart from Python, we will use these platforms:\n", + "\n", + "- Discord for notifications\n", + "- GitHub for hosting the app\n", + "- GitHub Actions for running the app on a schedule\n", + "- Supabase for hosting a free Postgres database instance" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building an Amazon Price Tracker App Step-by-step" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since this project involves multiple components working together, we'll take a top-down approach rather than building individual pieces first. This approach makes it easier to understand how everything fits together, since we'll introduce each tool only when it's needed. The benefits of this strategy will become clear as we progress through the tutorial." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Setting up the environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, let's create a dedicated environment on our machines to work on the project:\n", + "\n", + "```bash\n", + "mkdir automated-price-tracker\n", + "cd automated-price-tracker\n", + "python -m venv .venv\n", + "source .venv/bin/activate\n", + "```\n", + "\n", + "These commands create a working directory and activate a virtual environment. Next, create a new script called `ui.py` for designing the user interface with Streamlit.\n", + "\n", + "```bash\n", + "touch ui.py\n", + "```\n", + "\n", + "Then, install Streamlit:\n", + "\n", + "```bash\n", + "pip install streamlit\n", + "```\n", + "\n", + "Next, create a `requirements.txt` file and add Streamlit as the first dependency:\n", + "\n", + "```bash\n", + "touch requirements.txt\n", + "echo \"streamlit\" >> requirements.txt\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since the code will be hosted on GitHub, we need to initialize Git and create a `.gitignore` file:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "git init\n", + "touch .gitignore\n", + "echo \".venv\" >> .gitignore # Add the virtual env folder\n", + "git commit -m \"Initial commit\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Add a sidebar to the UI for product input" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at the final product one more time:\n", + "\n", + "![](images/sneak-peek.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It has two sections: the sidebar and the main dashboard. Since the first thing you do when launching this app is adding products, we will start building the sidebar first. Open `ui.py` and paste the following code:\n", + "\n", + "```python\n", + "import streamlit as st\n", + "\n", + "# Set up sidebar\n", + "with st.sidebar:\n", + " st.title(\"Add New Product\")\n", + " product_url = st.text_input(\"Product URL\")\n", + " add_button = st.button(\"Add Product\")\n", + "\n", + "# Main content\n", + "st.title(\"Price Tracker Dashboard\")\n", + "st.markdown(\"## Tracked Products\")\n", + "```\n", + "\n", + "The code snippet above sets up a basic Streamlit web application with two main sections. In the sidebar, it creates a form for adding new products with a text input field for the product URL and an \"Add Product\" button. The main content area contains a dashboard title and a section header for tracked products. The code uses Streamlit's `st.sidebar` context manager to create the sidebar layout and basic Streamlit components like `st.title`, `st.text_input`, and `st.button` to build the user interface elements.\n", + "\n", + "To see how this app looks like, run the following command:\n", + "\n", + "```bash\n", + "streamlit run ui.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's add a commit to save our progress:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a sidebar to the basic UI\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Add a feature to check if input URL is valid\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the next step, we want to add some restrictions to the input field like checking if the passed URL is valid. For this, create a new file called `utils.py` where we write additional utility functions for our app:\n", + "\n", + "```bash\n", + "touch utils.py\n", + "```\n", + "\n", + "Inside the script, paste following code:\n", + "\n", + "```bash\n", + "# utils.py\n", + "from urllib.parse import urlparse\n", + "import re\n", + "\n", + "\n", + "def is_valid_url(url: str) -> bool:\n", + " try:\n", + " # Parse the URL\n", + " result = urlparse(url)\n", + "\n", + " # Check if scheme and netloc are present\n", + " if not all([result.scheme, result.netloc]):\n", + " return False\n", + "\n", + " # Check if scheme is http or https\n", + " if result.scheme not in [\"http\", \"https\"]:\n", + " return False\n", + "\n", + " # Basic regex pattern for domain validation\n", + " domain_pattern = (\n", + " r\"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z]{2,})+$\"\n", + " )\n", + " if not re.match(domain_pattern, result.netloc):\n", + " return False\n", + "\n", + " return True\n", + "\n", + " except Exception:\n", + " return False\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above function `is_valid_url()` validates URLs by checking several criteria:\n", + "\n", + "1. It verifies the URL has both a scheme (`http`/`https`) and domain name\n", + "2. It ensures the scheme is specifically `http` or `https`\n", + "3. It validates the domain name format using regex to check for valid characters and TLD\n", + "4. It returns True only if all checks pass, False otherwise\n", + "\n", + "Let's use this function in our `ui.py` file. Here is the modified code:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import streamlit as st\n", + "from utils import is_valid_url\n", + "\n", + "\n", + "# Set up sidebar\n", + "with st.sidebar:\n", + " st.title(\"Add New Product\")\n", + " product_url = st.text_input(\"Product URL\")\n", + " add_button = st.button(\"Add Product\")\n", + "\n", + " if add_button:\n", + " if not product_url:\n", + " st.error(\"Please enter a product URL\")\n", + " elif not is_valid_url(product_url):\n", + " st.error(\"Please enter a valid URL\")\n", + " else:\n", + " st.success(\"Product is now being tracked!\")\n", + "\n", + "# Main content\n", + "...\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is what's new:\n", + "\n", + "1. We added URL validation using the `is_valid_url()` function from `utils.py`\n", + "2. When the button is clicked, we perform validation:\n", + " - Check if URL is empty\n", + " - Validate URL format using `is_valid_url()`\n", + "3. User feedback is provided through error/success messages:\n", + " - Error shown for empty URL\n", + " - Error shown for invalid URL format \n", + " - Success message when URL passes validation\n", + "\n", + "Rerun the Streamlit app again and see if our validation works. Then, return to your terminal to commit the changes we've made:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a feature to check URL validity\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Scrape the input URL for product details" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When a valid URL is entered and the add button is clicked, we need to implement product scraping functionality instead of just showing a success message. The system should:\n", + "\n", + "1. Immediately scrape the product URL to extract key details:\n", + " - Product name\n", + " - Current price\n", + " - Main product image\n", + " - Brand name\n", + " - Other relevant attributes\n", + "\n", + "2. Store these details in a database to enable:\n", + " - Regular price monitoring\n", + " - Historical price tracking\n", + " - Price change alerts\n", + " - Product status updates\n", + "\n", + "For the scraper, we will use [Firecrawl](firecrawl.dev), an AI-based scraping API for extracting webpage data without HTML parsing. This solution provides several advantages:\n", + "\n", + "1. No website HTML code analysis required for element selection\n", + "2. Resilient to HTML structure changes through AI-based element detection\n", + "3. Universal compatibility with product webpages due to structure-agnostic approach \n", + "4. Reliable website blocker bypass via robust API infrastructure\n", + "\n", + "First, create a new file called `scraper.py`:\n", + "\n", + "```bash\n", + "touch scraper.py\n", + "```\n", + "\n", + "Then, install these three libraries:\n", + "\n", + "```bash\n", + "pip install firecrawl-py pydantic python-dotenv\n", + "echo \"firecrawl-py\\npydantic\\npython-dotenv\\n\" >> requirements.txt # Add them to dependencies\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`firecrawl-py` is the Python SDK for Firecrawl scraping engine, `pydantic` is a data validation library that helps enforce data types and structure through Python class definitions, and `python-dotenv` is a library that loads environment variables from a `.env` file into your Python application.\n", + "\n", + "With that said, head over to the Firecrawl website and [sign up for a free account](https://www.firecrawl.dev/) (the free plan will work fine). You will be given an API key, which you should copy. \n", + "\n", + "Then, create a `.env` file in your terminal and add the API key as an environment variable:\n", + "\n", + "```bash\n", + "touch .env\n", + "echo \"FIRECRAWL_API_KEY='YOUR-API-KEY-HERE' >> .env\"\n", + "echo \".env\" >> .gitignore # Ignore .env files in Git\n", + "```\n", + "\n", + "The `.env` file is used to securely store sensitive configuration values like API keys that shouldn't be committed to version control. By storing the Firecrawl API key in `.env` and adding it to `.gitignore`, we ensure it stays private while still being accessible to our application code. This is a security best practice to avoid exposing credentials in source control.\n", + "\n", + "Now, we can start writing the `scraper.py`:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "from firecrawl import FirecrawlApp\n", + "from pydantic import BaseModel, Field\n", + "from dotenv import load_dotenv\n", + "from datetime import datetime\n", + "\n", + "load_dotenv()\n", + "\n", + "app = FirecrawlApp()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here, `load_dotenv()` function reads the `.env` file you have in your working directory and loads the environment variables inside, including the Firecrawl API key. When you create an instance of `FirecrawlApp` class, the API key is automatically detected to establish a connection between your script and the scraping engine in the form of the `app` variable.\n", + "\n", + "Now, we create a Pydantic class (usually called a model) that defines the details we want to scrape from each product:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "class Product(BaseModel):\n", + " \"\"\"Schema for creating a new product\"\"\"\n", + "\n", + " url: str = Field(description=\"The URL of the product\")\n", + " name: str = Field(description=\"The product name/title\")\n", + " price: float = Field(description=\"The current price of the product\")\n", + " currency: str = Field(description=\"Currency code (USD, EUR, etc)\")\n", + " main_image_url: str = Field(description=\"The URL of the main image of the product\")\n", + "```\n", + "\n", + "Pydantic models may be completely new to you, so let's break down the `Product` model:\n", + "\n", + "- The `url` field stores the product page URL we want to track\n", + "- The `name` field stores the product title/name that will be scraped\n", + "- The `price` field stores the current price as a float number\n", + "- The `currency` field stores the 3-letter currency code (e.g. USD, EUR)\n", + "- The `main_image_url` field stores the URL of the product's main image\n", + "\n", + "Each field is typed and has a description that documents its purpose. The `Field` class from Pydantic allows us to add metadata like descriptions to each field. These descriptions are especially important for Firecrawl since it uses them to automatically locate the relevant HTML elements containing the data we want. \n", + "\n", + "Now, let's create a function to call the engine to scrape URL's based on the schema above:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "def scrape_product(url: str):\n", + " extracted_data = app.scrape_url(\n", + " url,\n", + " params={\n", + " \"formats\": [\"extract\"],\n", + " \"extract\": {\"schema\": Product.model_json_schema()},\n", + " },\n", + " )\n", + "\n", + " # Add the scraping date to the extracted data\n", + " extracted_data[\"extract\"][\"timestamp\"] = datetime.utcnow()\n", + "\n", + " return extracted_data[\"extract\"]\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " product = \"https://www.amazon.com/gp/product/B002U21ZZK/\"\n", + "\n", + " print(scrape_product(product))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code above defines a function called `scrape_product` that takes a URL as input and uses it to scrape product information. Here's how it works:\n", + "\n", + "The function calls `app.scrape_url` with two parameters:\n", + "1. The product URL to scrape\n", + "2. A params dictionary that configures the scraping:\n", + " - It specifies we want to use the \"extract\" format\n", + " - It provides our `Product` Pydantic model schema as the extraction template as a JSON object\n", + "\n", + "The scraper will attempt to find and extract data that matches our Product schema fields - the URL, name, price, currency, and image URL.\n", + "\n", + "The function returns just the \"extract\" portion of the scraped data, which contains the structured product information. `extract` returns a dictionary to which we add the date of the scraping as it will be important later on.\n", + "\n", + "Let's test the script by running it:\n", + "\n", + "```bash\n", + "python scraper.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should get an output like this:\n", + "\n", + "```python\n", + "{\n", + " 'url': 'https://www.amazon.com/dp/B002U21ZZK', \n", + " 'name': 'MOVA Globe Earth with Clouds 4.5\"', \n", + " 'price': 212, \n", + " 'currency': 'USD', \n", + " 'main_image_url': 'https://m.media-amazon.com/images/I/41bQ3Y58y3L._AC_.jpg', \n", + " 'timestamp': '2024-12-05 13-20'\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output shows that a [MOVA Globe](https://www.amazon.com/dp/B002U21ZZK) costs $212 USD on Amazon at the time of writing this article. You can test the script for any other website that contains the information we are looking (except Ebay):\n", + "\n", + "- Price\n", + "- Product name/title\n", + "- Main image URL\n", + "\n", + "One key advantage of using Firecrawl is that it returns data in a consistent dictionary format across all websites. Unlike HTML-based scrapers like BeautifulSoup or Scrapy which require custom code for each site and can break when website layouts change, Firecrawl uses AI to understand and extract the requested data fields regardless of the underlying HTML structure. \n", + "\n", + "Finish this step by committing the new changes to Git:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Implement a Firecrawl scraper for products\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5: Storing new products in a PostgreSQL database" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we want to check product prices regularly, we need to have an online database. In this case, Postgres is the best option since it's reliable, scalable, and has great support for storing time-series data like price histories.\n", + "\n", + "There are many platforms for hosting Postgres instances but the one I find the easiest and fastest to set up is Supabase. So, please head over to [the Supabase website](https://supabase.com) and create your free account. During the sign-up process, you will be given a password, which you should save somewhere safe on your machine. \n", + "\n", + "\n", + "Then, in a few minutes, your free Postgres instance comes online. To connect to this instance, click on Home in the left sidebar and then, \"Connect\":\n", + "\n", + "![](images/supabase_connect.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You will be shown your database connection string with a placeholder for the password you copied. You should paste this string in your `.env` file with your password added to the `.env` file:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "echo POSTGRES_URL=\"THE-SUPABASE-URL-STRING-WITH-YOUR-PASSWORD-ADDED\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, the easiest way to interact with this database is through SQLAlchemy. Let's install it:\n", + "\n", + "```bash\n", + "pip install \"sqlalchemy==2.0.35\" psycopg2-binary\n", + "echo \"psycopg2-binary\\nsqlalchemy==2.0.35\" >> requirements.txt\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Note: [SQLAlchemy](https://sqlalchemy.org) is a Python SQL toolkit and Object-Relational Mapping (ORM) library that lets us interact with databases using Python code instead of raw SQL. For our price tracking project, it provides essential features like database connection management, schema definition through Python classes, and efficient querying capabilities. This makes it much easier to store and retrieve product information and price histories in our Postgres database.\n", + "\n", + "After the installation, create a new `database.py` file for storing database-related functions:\n", + "\n", + "```bash\n", + "touch database.py\n", + "```\n", + "\n", + "Let's populate this script:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "from sqlalchemy import create_engine, Column, String, Float, DateTime, ForeignKey\n", + "from sqlalchemy.orm import sessionmaker, relationship, declarative_base\n", + "from datetime import datetime\n", + "\n", + "Base = declarative_base()\n", + "\n", + "\n", + "class Product(Base):\n", + " __tablename__ = \"products\"\n", + "\n", + " url = Column(String, primary_key=True)\n", + " prices = relationship(\n", + " \"PriceHistory\", back_populates=\"product\", cascade=\"all, delete-orphan\"\n", + " )\n", + "\n", + "\n", + "class PriceHistory(Base):\n", + " __tablename__ = \"price_histories\"\n", + "\n", + " id = Column(String, primary_key=True)\n", + " product_url = Column(String, ForeignKey(\"products.url\"))\n", + " name = Column(String, nullable=False)\n", + " price = Column(Float, nullable=False)\n", + " currency = Column(String, nullable=False)\n", + " main_image_url = Column(String)\n", + " timestamp = Column(DateTime, nullable=False)\n", + " product = relationship(\"Product\", back_populates=\"prices\")\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "The code above defines two SQLAlchemy models for our price tracking database:\n", + "\n", + "The Product model represents items we want to track, with the product URL as the primary key. It has a one-to-many relationship with price histories (which means each product in `products` can have multiple price history entry in `price_histories`).\n", + "\n", + "The `PriceHistory` model stores individual price points over time. Each record contains:\n", + "- A unique ID as primary key\n", + "- The product URL as a foreign key linking to the `Product`\n", + "- The product name\n", + "- The price value and currency\n", + "- The main product image URL\n", + "- A timestamp of when the price was recorded\n", + "\n", + "The relationship between `Product` and `PriceHistory` is bidirectional, allowing easy navigation between related records. The `cascade` setting ensures price histories are deleted when their product is deleted.\n", + "\n", + "These models provide the structure for storing and querying our price tracking data in a PostgreSQL database using SQLAlchemy's ORM capabilities." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we define a `Database` class with a singe `add_product` method:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "class Database:\n", + " def __init__(self, connection_string):\n", + " self.engine = create_engine(connection_string)\n", + " Base.metadata.create_all(self.engine)\n", + " self.Session = sessionmaker(bind=self.engine)\n", + "\n", + " def add_product(self, url):\n", + " session = self.Session()\n", + " try:\n", + " # Create the product entry\n", + " product = Product(url=url)\n", + " session.merge(product) # merge will update if exists, insert if not\n", + " session.commit()\n", + " finally:\n", + " session.close()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "The `Database` class above provides core functionality for managing product data in our PostgreSQL database. It takes a connection string in its constructor to establish the database connection using SQLAlchemy.\n", + "\n", + "The `add_product` method allows us to store new product URLs in the database. It uses SQLAlchemy's `merge` functionality which intelligently handles both inserting new products and updating existing ones, preventing duplicate entries.\n", + "\n", + "The method carefully manages database sessions, ensuring proper resource cleanup by using `try`/`finally` blocks. This prevents resource leaks and maintains database connection stability." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use this method inside the sidebar of our UI. Switch to `ui.py` and make the following adjustments:\n", + "\n", + "First, update the imports to load the Database class and initialize it:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import os\n", + "import streamlit as st\n", + "\n", + "from utils import is_valid_url\n", + "from database import Database\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "\n", + "with st.spinner(\"Loading database...\"):\n", + " db = Database(os.getenv(\"POSTGRES_URL\"))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code integrates the `Database` class into the Streamlit UI by importing required dependencies and establishing a database connection. The database URL is loaded securely from environment variables using `python-dotenv`. The `Database` class creates or updates the tables we specified in `database.py` after being initialized.\n", + "\n", + "The database initialization process is wrapped in a Streamlit spinner component to maintain responsiveness while establishing the connection. This provides visual feedback during the connection setup period, which typically requires a brief initialization time." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, in the sidebar code, we only need to add a single line of code to add the product to the database if the URL is valid:\n", + "\n", + "```python\n", + "# Set up sidebar\n", + "with st.sidebar:\n", + " st.title(\"Add New Product\")\n", + " product_url = st.text_input(\"Product URL\")\n", + " add_button = st.button(\"Add Product\")\n", + "\n", + " if add_button:\n", + " if not product_url:\n", + " st.error(\"Please enter a product URL\")\n", + " elif not is_valid_url(product_url):\n", + " st.error(\"Please enter a valid URL\")\n", + " else:\n", + " db.add_product(product_url) # This is the new line\n", + " st.success(\"Product is now being tracked!\")\n", + "```\n", + "\n", + "In the final `else` block that runs when the product URL is valid, we call the `add_product` method to store the product in the database.\n", + "\n", + "Let's commit everything:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a Postgres database integration for tracking product URLs\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 6: Storing price histories for new products" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, after the product is added to the `products` table, we want to add its details and its scraped price to the `price_histories` table. \n", + "\n", + "First, switch to `database.py` and add a new method for creating entries in the `PriceHistories` table:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "class Database:\n", + " ... # the rest of the class\n", + "\n", + " def add_price(self, product_data):\n", + " session = self.Session()\n", + " try:\n", + " price_history = PriceHistory(\n", + " id=f\"{product_data['url']}_{product_data['timestamp']}\",\n", + " product_url=product_data[\"url\"],\n", + " name=product_data[\"name\"],\n", + " price=product_data[\"price\"],\n", + " currency=product_data[\"currency\"],\n", + " main_image_url=product_data[\"main_image_url\"],\n", + " timestamp=product_data[\"timestamp\"],\n", + " )\n", + " session.add(price_history)\n", + " session.commit()\n", + " finally:\n", + " session.close()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `add_price` method takes a dictionary containing product data (which is returned by our scraper) and creates a new entry in the `PriceHistory` table. The entry's ID is generated by combining the product URL with a timestamp. The method stores essential product information like name, price, currency, image URL, and the timestamp of when the price was recorded. It uses SQLAlchemy's session management to safely commit the new price history entry to the database.\n", + "\n", + "Now, we need to add this functionality to the sidebar as well. In `ui.py`, add a new import statement that loads the `scrape_product` function from `scraper.py`:\n", + "\n", + "```python\n", + "... # The rest of the imports\n", + "from scraper import scrape_product\n", + "```\n", + "\n", + "Then, update the `else` block in the sidebar again:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "with st.sidebar:\n", + " st.title(\"Add New Product\")\n", + " product_url = st.text_input(\"Product URL\")\n", + " add_button = st.button(\"Add Product\")\n", + "\n", + " if add_button:\n", + " if not product_url:\n", + " st.error(\"Please enter a product URL\")\n", + " elif not is_valid_url(product_url):\n", + " st.error(\"Please enter a valid URL\")\n", + " else:\n", + " db.add_product(product_url)\n", + " with st.spinner(\"Added product to database. Scraping product data...\"):\n", + " product_data = scrape_product(product_url)\n", + " db.add_price(product_data)\n", + " st.success(\"Product is now being tracked!\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now when a user enters a product URL and clicks the \"Add Product\" button, several things happen:\n", + "\n", + "1. The URL is validated to ensure it's not empty and is properly formatted.\n", + "2. If valid, the URL is added to the products table via `add_product()`.\n", + "3. The product page is scraped immediately to get current price data.\n", + "4. This initial price data is stored in the price history table via `add_price()`.\n", + "5. The user sees loading spinners and success messages throughout the process.\n", + "\n", + "This gives us a complete workflow for adding new products to track, including capturing their initial price point. The UI provides clear feedback at each step and handles errors gracefully.\n", + "\n", + "Check that everything is working the way we want it and then, commit the new changes:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a feature to track product prices after they are added\"\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 7: Displaying each product's price history in the main dashboard" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at the final product shown in the introduction once again:\n", + "\n", + "![](images/sneak-peek.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Apart from the sidebar, the main dashboard shows each product's price history visualized with a Plotly line plot where the X axis is the timestamp while the Y axis is the prices. Each line plot is wrapped in a Streamlit component that includes buttons for removing the product from the database or visiting its source URL. \n", + "\n", + "In this step, we will implement the plotting feature and leave the two buttons for a later section. First, add a new method to the `Database` class for retrieving the price history for each product:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "class Database:\n", + " ... # The rest of the code\n", + "\n", + " def get_price_history(self, url):\n", + " \"\"\"Get price history for a product\"\"\"\n", + " session = self.Session()\n", + " try:\n", + " return (\n", + " session.query(PriceHistory)\n", + " .filter(PriceHistory.product_url == url)\n", + " .order_by(PriceHistory.timestamp.desc())\n", + " .all()\n", + " )\n", + " finally:\n", + " session.close()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The method queries the price histories table based on product URL, orders the rows in descending order (oldest first) and returns the results. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, add another method for retrieving all products from the `products` table:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "class Database:\n", + " ...\n", + " \n", + " def get_all_products(self):\n", + " session = self.Session()\n", + " try:\n", + " return session.query(Product).all()\n", + " finally:\n", + " session.close()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The idea is that every time our Streamlit app is opened, the main dashboard queries all existing products from the database and render their price histories with line charts in dedicated components. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To create the line charts, we need Plotly and Pandas, so install them in your environment:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "pip install pandas plotly\n", + "echo \"pandas\\nplotly\" >> requirements.txt\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Afterward, import them at the top of `ui.py` along with other existing imports:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import pandas as pd\n", + "import plotly.express as px\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, switch to `ui.py` and paste the following snippet of code after the Main content section:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "# Main content\n", + "st.title(\"Price Tracker Dashboard\")\n", + "st.markdown(\"## Tracked Products\")\n", + "\n", + "# Get all products\n", + "products = db.get_all_products()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here, after the page title and subtitle is shown, we are retrieving all products from the database. Let's loop over them:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "# Create a card for each product\n", + "for product in products:\n", + " price_history = db.get_price_history(product.url)\n", + " if price_history:\n", + " # Create DataFrame for plotting\n", + " df = pd.DataFrame(\n", + " [\n", + " {\"timestamp\": ph.timestamp, \"price\": ph.price, \"name\": ph.name}\n", + " for ph in price_history\n", + " ]\n", + " )\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For each product, we get their price history with `db.get_price_history` and then, convert this data into a dataframe with three columns:\n", + "\n", + "- Timestamp\n", + "- Price\n", + "- Product name\n", + "\n", + "This makes plotting easier with Plotly. Next, we create a Streamlit expander component for each product:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "# Create a card for each product\n", + "for product in products:\n", + " price_history = db.get_price_history(product.url)\n", + " if price_history:\n", + " ...\n", + " # Create a card-like container for each product\n", + " with st.expander(df[\"name\"][0], expanded=False):\n", + " st.markdown(\"---\")\n", + " col1, col2 = st.columns([1, 3])\n", + "\n", + " with col1:\n", + " if price_history[0].main_image_url:\n", + " st.image(price_history[0].main_image_url, width=200)\n", + " st.metric(\n", + " label=\"Current Price\",\n", + " value=f\"{price_history[0].price} {price_history[0].currency}\",\n", + " )\n", + "```\n", + "\n", + "The expander shows the product name as its title and contains:\n", + "\n", + "1. A divider line\n", + "2. Two columns:\n", + " - Left column: Product image (if available) and current price metric\n", + " - Right column (shown in next section)\n", + "\n", + "The price is displayed using Streamlit's metric component which shows the current price and currency.\n", + "\n", + "Here is the rest of the code:\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + " ...\n", + " \n", + " with col2:\n", + " # Create price history plot\n", + " fig = px.line(\n", + " df,\n", + " x=\"timestamp\",\n", + " y=\"price\",\n", + " title=None,\n", + " )\n", + " fig.update_layout(\n", + " xaxis_title=None,\n", + " yaxis_title=\"Price ($)\",\n", + " showlegend=False,\n", + " margin=dict(l=0, r=0, t=0, b=0),\n", + " height=300,\n", + " )\n", + " fig.update_xaxes(tickformat=\"%Y-%m-%d %H:%M\", tickangle=45)\n", + " fig.update_yaxes(tickprefix=\"$\", tickformat=\".2f\")\n", + " st.plotly_chart(fig, use_container_width=True)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the right column, we create an interactive line plot using Plotly Express to visualize the price history over time. The plot shows price on the y-axis and timestamp on the x-axis. The layout is customized to remove the title, adjust axis labels and formatting, and optimize the display size. The timestamps are formatted to show date and time, with angled labels for better readability. Prices are displayed with 2 decimal places and a dollar sign prefix. The plot is rendered using Streamlit's `plotly_chart` component and automatically adjusts its width to fill the container.\n", + "\n", + "After this step, the UI must be fully functional and ready to track products. For example, here is what mine looks like after adding a couple of products:\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](images/finished.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But notice how the price history chart doesn't show anything. That's because we haven't populated it by checking the product price in regular intervals. Let's do that in the next couple of steps. For now, commit the latest changes we've made:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Display product price histories for each product in the dashboard\"\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "------------\n", + "\n", + "Let's take a brief moment to summarize the steps we took so far and what's next. So far, we've built a Streamlit interface that allows users to add product URLs and displays their current prices and basic information. We've implemented the database schema, created functions to scrape product data, and designed a clean UI with price history visualization. The next step is to set up automated price checking to populate our history charts and enable proper price tracking over time.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 8: Adding new price entries for existing products" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we want to write a script that adds new price entries in the `price_histories` table for each product in `products` table. We call this script `check_prices.py`:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import os\n", + "from database import Database\n", + "from dotenv import load_dotenv\n", + "from firecrawl import FirecrawlApp\n", + "from scraper import scrape_product\n", + "\n", + "load_dotenv()\n", + "\n", + "db = Database(os.getenv(\"POSTGRES_URL\"))\n", + "app = FirecrawlApp()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At the top, we are importing the functions and packages and initializing the database and a Firecrawl app. Then, we define a simple `check_prices` function:\n", + "\n", + "```python\n", + "def check_prices():\n", + " products = db.get_all_products()\n", + "\n", + " for product in products:\n", + " # Retrieve updated product data\n", + " updated_product = scrape_product(product.url)\n", + "\n", + " # Add the price to the database\n", + " db.add_price(updated_product)\n", + " print(f\"Added new price entry for {updated_product['name']}\")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_prices()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the function body, we retrieve all products URLs, retrieve their new price data with `scrape_product` function from `scraper.py` and then, add a new price entry for the product with `db.add_price`. \n", + "\n", + "If you run the function once and refresh the Streamlit app, you must see a line chart appear for each product you are tracking:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](images/linechart.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's commit the changes in this step:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a script for checking prices of existing products\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 9: Check prices regularly with GitHub actions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate various software workflows directly from your GitHub repository. In our case, it's particularly useful because we can set up automated price checks to run the `check_prices.py` script at regular intervals (e.g., daily or hourly) without manual intervention. This ensures we consistently track price changes and maintain an up-to-date database of historical prices for our tracked products.\n", + "\n", + "So, the first step is creating a new GitHub repository for our project and pushing existing code to it:\n", + "\n", + "```bash\n", + "git remote add origin https://github.com/yourusername/price-tracker.git\n", + "git push origin main\n", + "```\n", + "\n", + "Then, return to your terminal and create this directory structure:\n", + "\n", + "```bash\n", + "mkdir -p .github/workflows\n", + "touch .github/workflows/check_prices.yml\n", + "```\n", + "\n", + "The first command creates a new directory structure `.github/workflows` using the `-p` flag to create parent directories if they don't exist.\n", + "\n", + "The second command creates an empty YAML file called `check_prices.yml` inside the workflows directory. GitHub Actions looks for workflow files in this specific location - any YAML files in the `.github/workflows` directory will be automatically detected and processed as workflow configurations. These YAML files define when and how your automated tasks should run, what environment they need, and what commands to execute. In our case, this file will contain instructions for GitHub Actions to periodically run our price checking script. Let's write it:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```yaml\n", + "name: Price Check\n", + "\n", + "on:\n", + " schedule:\n", + " # Runs every 3 minutes\n", + " - cron: \"*/3 * * * *\"\n", + " workflow_dispatch: # Allows manual triggering\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's break down this first part of the YAML file:\n", + "\n", + "The `name: Price Check` line gives our workflow a descriptive name that will appear in the GitHub Actions interface.\n", + "\n", + "The `on:` section defines when this workflow should be triggered. We've configured two triggers:\n", + "\n", + "1. A schedule using cron syntax `*/3 * * * *` which runs the workflow every 3 minutes. The five asterisks represent minute, hour, day of month, month, and day of week respectively. The `*/3` means \"every 3rd minute\". The 3-minute interval is for debugging purposes, we will need to choose a wider interval later on to respect the free limits of GitHub actions. \n", + "\n", + "2. `workflow_dispatch` enables manual triggering of the workflow through the GitHub Actions UI, which is useful for testing or running the check on-demand.\n", + "\n", + "Now, let's add the rest:\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```yaml\n", + "jobs:\n", + " check-prices:\n", + " runs-on: ubuntu-latest\n", + "\n", + " steps:\n", + " - name: Checkout code\n", + " uses: actions/checkout@v4\n", + "\n", + " - name: Set up Python\n", + " uses: actions/setup-python@v5\n", + " with:\n", + " python-version: \"3.10\"\n", + " cache: \"pip\"\n", + "\n", + " - name: Install dependencies\n", + " run: |\n", + " python -m pip install --upgrade pip\n", + " pip install -r automated_price_tracking/requirements.txt\n", + "\n", + " - name: Run price checker\n", + " env:\n", + " FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}\n", + " POSTGRES_URL: ${{ secrets.POSTGRES_URL }}\n", + " run: python automated_price_tracking/check_prices.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's break down this second part of the YAML file:\n", + "\n", + "The `jobs:` section defines the actual work to be performed. We have one job named `check-prices` that runs on an Ubuntu virtual machine (`runs-on: ubuntu-latest`).\n", + "\n", + "Under `steps:`, we define the sequence of actions:\n", + "\n", + "1. First, we checkout our repository code using the standard `actions/checkout@v4` action\n", + "\n", + "2. Then we set up Python 3.10 using `actions/setup-python@v5`, enabling pip caching to speed up dependency installation\n", + "\n", + "3. Next, we install our Python dependencies by upgrading `pip` and installing requirements from our `requirements.txt` file. At this point, it is essential that you were keeping a complete dependency file based on the installs we made in the project. \n", + "\n", + "4. Finally, we run our price checker script, providing two environment variables:\n", + " - `FIRECRAWL_API_KEY`: For accessing the web scraping service\n", + " - `POSTGRES_URL`: For connecting to our database\n", + "\n", + "Both variables must be stored in our GitHub repository as secrets for this workflow file to run without errors. So, navigate to the repository you've created for the project and open its Settings. Under \"Secrets and variables\" > \"Actions\", click on \"New repository secret\" button to add the environment variables we have in the `.env` file one-by-one. \n", + "\n", + "Then, return to your terminal, commit the changes and push:\n", + "\n", + "```bash\n", + "git add . \n", + "git commit -m \"Add a workflow to check prices regularly\"\n", + "git push origin main\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, navigate to your GitHub repository again and click on the \"Actions\" tab:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](images/actions.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From there, you can run the workflow manually (click \"Run workflow\" and refresh the page). If it is executed successfully, you can return to the Streamlit app and refresh to see the new price added to the chart." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 10: Setting up Discord for notifications" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we know our scheduling workflow works, the first order of business is setting a wider check interval in the workflow file. Even though our first workflow run was manually, the rest happen automatically." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "on:\n", + " schedule:\n", + " # Runs every 6 hours\n", + " - cron: \"0 0,6,12,18 * * *\"\n", + " workflow_dispatch: # Allows manual triggering\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the workflow file, change the cron field to the syntax you see above, which runs the workflow at the first minute of 12am, 6am, 12pm and 6pm UTC. Then, commit and push the changes:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Set a wider check interval in the workflow file\"\n", + "git push origin main\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now comes the interesting part. Each time the workflow is run, we want to compare the current price of the product to its original price when we started tracking it. If the difference between these two prices is below a certain threshold like 5%, this means there is a discount happening for the product and we want to send a notification. \n", + "\n", + "The easiest way to set this up is by using Discord webhooks. So, if you haven't got one already, go to Discord.com and create a new account (optionally, download the desktop app as well). Then, log in to your account and you will find a \"Plus\" button in the bottom-left corner. Click on it to create your own Discord server:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](images/discord.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After pressing \"Plus\", choose \"Create my own\" and \"For me and my friends\". Then, give a new name to your server and you will be presented with an empty channel:\n", + "\n", + "![](images/new-server.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Right click on \"general\" and choose \"Edit channel\". Switch to the integrations tab and click on \"Create webhook\". Discord immediately generates a new webhook with a random name and you should copy its URL. \n", + "\n", + "![](images/webhook.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Webhooks are automated messages sent from apps to other apps in real-time. They work like a notification system - when something happens in one app, it automatically sends data to another app through a unique URL. In our case, we'll use Discord webhooks to automatically notify us when there's a price drop. Whenever our price tracking script detects a significant discount, it will send a message to our Discord channel through the webhook URL, ensuring we never miss a good deal.\n", + "\n", + "After copying the webhook URL, you should save it as environment variable to your `.env` file:\n", + "\n", + "```python\n", + "echo \"DISCORD_WEBHOOK_URL='THE-URL-YOU-COPIED'\" >> .env\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, create a new file called `notifications.py` and paste the following contents:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "from dotenv import load_dotenv\n", + "import os\n", + "import aiohttp\n", + "import asyncio\n", + "\n", + "load_dotenv()\n", + "\n", + "\n", + "async def send_price_alert(\n", + " product_name: str, old_price: float, new_price: float, url: str\n", + "):\n", + " \"\"\"Send a price drop alert to Discord\"\"\"\n", + " drop_percentage = ((old_price - new_price) / old_price) * 100\n", + "\n", + " message = {\n", + " \"embeds\": [\n", + " {\n", + " \"title\": \"Price Drop Alert! 🎉\",\n", + " \"description\": f\"**{product_name}**\\nPrice dropped by {drop_percentage:.1f}%!\\n\"\n", + " f\"Old price: ${old_price:.2f}\\n\"\n", + " f\"New price: ${new_price:.2f}\\n\"\n", + " f\"[View Product]({url})\",\n", + " \"color\": 3066993,\n", + " }\n", + " ]\n", + " }\n", + "\n", + " try:\n", + " async with aiohttp.ClientSession() as session:\n", + " await session.post(os.getenv(\"DISCORD_WEBHOOK_URL\"), json=message)\n", + " except Exception as e:\n", + " print(f\"Error sending Discord notification: {e}\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `send_price_alert` function above is responsible for sending price drop notifications to Discord using webhooks. Let's break down what's new:\n", + "\n", + "1. The function takes 4 parameters:\n", + " - `product_name`: The name of the product that dropped in price\n", + " - `old_price`: The previous price before the drop\n", + " - `new_price`: The current lower price\n", + " - `url`: Link to view the product\n", + "\n", + "2. It calculates the percentage drop in price using the formula: `((old_price - new_price) / old_price) * 100`\n", + "\n", + "3. The notification is formatted as a Discord embed - a rich message format that includes:\n", + " - A title with a celebration emoji\n", + " - A description showing the product name, price drop percentage, old and new prices\n", + " - A link to view the product\n", + " - A green color (3066993 in decimal)\n", + "\n", + "4. The message is sent asynchronously using `aiohttp` to post to the Discord webhook URL stored in the environment variables\n", + "\n", + "5. Error handling is included to catch and print any issues that occur during the HTTP request" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "This provides a clean way to notify users through Discord whenever we detect a price drop for tracked products.\n", + "\n", + "To check the notification system works, add this main block to the end of the script:\n", + "\n", + "```python\n", + "if __name__ == \"__main__\":\n", + " asyncio.run(send_price_alert(\"Test Product\", 100, 90, \"https://www.google.com\"))\n", + "```\n", + "\n", + "`asyncio.run()` is used here because `send_price_alert` is an async function that needs to be executed in an event loop. `asyncio.run()` creates and manages this event loop, allowing the async HTTP request to be made properly. Without it, we wouldn't be able to use the `await` keyword inside `send_price_alert`.\n", + "\n", + "\n", + "To run the script, install `aiohttp`:\n", + "\n", + "```python\n", + "pip install aiohttp\n", + "echo \"aiohttp\\n\" >> requirements.txt\n", + "python notifications.py\n", + "```\n", + "\n", + "If all is well, you should get a Discord message in your server that looks like this:\n", + "\n", + "![](images/alert.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's commit the changes we have again:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Set up Discord alert system\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 11: Sending Discord alerts when prices drop" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, the only step left is adding a price comparison logic to `check_prices.py`. In other words, we want to use the `send_price_alert` function if the new scraped price is lower than the original. This requires a revamped `check_prices.py` script:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import os\n", + "import asyncio\n", + "from database import Database\n", + "from dotenv import load_dotenv\n", + "from firecrawl import FirecrawlApp\n", + "from scraper import scrape_product\n", + "from notifications import send_price_alert\n", + "\n", + "load_dotenv()\n", + "\n", + "db = Database(os.getenv(\"POSTGRES_URL\"))\n", + "app = FirecrawlApp()\n", + "\n", + "# Threshold percentage for price drop alerts (e.g., 5% = 0.05)\n", + "PRICE_DROP_THRESHOLD = 0.05\n", + "\n", + "\n", + "async def check_prices():\n", + " products = db.get_all_products()\n", + " product_urls = set(product.url for product in products)\n", + "\n", + " for product_url in product_urls:\n", + " # Get the price history\n", + " price_history = db.get_price_history(product_url)\n", + " if not price_history:\n", + " continue\n", + "\n", + " # Get the earliest recorded price\n", + " earliest_price = price_history[-1].price\n", + "\n", + " # Retrieve updated product data\n", + " updated_product = scrape_product(product_url)\n", + " current_price = updated_product[\"price\"]\n", + "\n", + " # Add the price to the database\n", + " db.add_price(updated_product)\n", + " print(f\"Added new price entry for {updated_product['name']}\")\n", + "\n", + " # Check if price dropped below threshold\n", + " if earliest_price > 0: # Avoid division by zero\n", + " price_drop = (earliest_price - current_price) / earliest_price\n", + " if price_drop >= PRICE_DROP_THRESHOLD:\n", + " await send_price_alert(\n", + " updated_product[\"name\"], earliest_price, current_price, product_url\n", + " )\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " asyncio.run(check_prices())\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's examine the key changes in this enhanced version of `check_prices.py`:\n", + "\n", + "1. New imports and setup\n", + " - Added `asyncio` for `async`/`await` support\n", + " - Imported `send_price_alert` from `notifications.py`\n", + " - Defined `PRICE_DROP_THRESHOLD = 0.05` (5% threshold for alerts)\n", + "\n", + "2. Async function conversion\n", + " - Converted `check_prices()` to async function\n", + " - Gets unique product URLs using set comprehension to avoid duplicates\n", + " \n", + "3. Price history analysis\n", + " - Retrieves full price history for each product\n", + " - Gets `earliest_price` from `history[-1]` (works because we ordered by timestamp DESC)\n", + " - Skips products with no price history using `continue`\n", + " \n", + "4. Price drop detection logic\n", + " - Calculates drop percentage: `(earliest_price - current_price) / earliest_price`\n", + " - Checks if drop exceeds 5% threshold\n", + " - Sends Discord alert if threshold exceeded using `await send_price_alert()`\n", + " \n", + "5. Async main block\n", + " - Uses `asyncio.run()` to execute async `check_prices()` in event loop\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When I tested this new version of the script, I immediately got an alert:\n", + "\n", + "![](images/new-alert.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's commit everything and push to GitHub so that our workflow is supercharged with our notification system:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add notification system to price drops\"\n", + "git push origin main\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion and Next Steps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Congratulations for making it to the end of this extremely long tutorial! We've just covered how to implement an end-to-end Python project you can proudly showcase on your portfolio. We built a complete price tracking system that scrapes product data from e-commerce websites, stores it in a Postgres database, analyzes price histories, and sends automated Discord notifications when prices drop significantly. Along the way, we learned about web scraping with Firecrawl, database management with SQLAlchemy, asynchronous programming with asyncio, building interactive UIs with Streamlit, automating with GitHub actions and integrating external webhooks.\n", + "\n", + "However, the project is far from perfect. Since we took a top-down approach to building this app, our project code is scattered across multiple files and doesn't conform to programming best practices most of the time. For this reason, I've recreated the same project in a much more sophisticated matter with production-level features. [This new version on GitHub](https://github.com/BexTuychiev/automated-price-tracking) implements proper database session management, faster operations and overall smoother user experience. \n", + "\n", + "If you decide to stick with the basic version, you can find the full project code and the notebook from the official Firecrawl GitHub repository example projects. I also recommend that you deploy your Streamlit app to Streamlit Cloud so that you have a function app accessible everywhere you go. \n", + "\n", + "Here are some more guides from our blog if you are interested:\n", + "\n", + "- [How to Run Web Scrapers on Schedule](https://www.firecrawl.dev/blog/automated-web-scraping-free-2025)\n", + "- [More about using Firecrawl's `scrape_url` function](https://www.firecrawl.dev/blog/mastering-firecrawl-scrape-endpoint)\n", + "- [Scraping entire websites with Firecrawl in a single command - the /crawl endpoint](https://www.firecrawl.dev/blog/mastering-the-crawl-endpoint-in-firecrawl)\n", + "\n", + "Thank you for reading!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/blog-articles/amazon-price-tracking/notebook.md b/examples/blog-articles/amazon-price-tracking/notebook.md new file mode 100644 index 00000000..38744828 --- /dev/null +++ b/examples/blog-articles/amazon-price-tracking/notebook.md @@ -0,0 +1,1237 @@ +--- +title: How to Build an Automated Amazon Price Tracking Tool in Python For Free +description: Learn how to build a free automated price tracking tool in Python that monitors Amazon and other e-commerce sites, sends Discord alerts for price drops, and maintains price history using Firecrawl, Streamlit, and GitHub Actions. +slug: amazon-price-tracker-in-python-for-free +date: Dec 6, 2024 +author: bex_tuychiev +image: /images/blog/amazon-price-tracking/amazon-price-tracker-in-python-for-free.jpg +categories: [tutorials] +keywords: [amazon price tracker, amazon price history tracker, amazon price tracker app, amazon web scraper, amazon web scraper python, ecommerce web scraping, web scraping python] +--- + +## That sends alerts to your phone and keeps price history + +## What Shall We Build in This Tutorial? + +There is a lot to be said about the psychology of discounts. For example, buying a discounted item we don't need isn't saving money at all - it's falling for one of the oldest sales tactics. However, there are legitimate cases where waiting for a price drop on items you actually need makes perfect sense. + +The challenge is that e-commerce websites run flash sales and temporary discounts constantly, but these deals often disappear as quickly as they appear. Missing these brief windows of opportunity can be frustrating. + +That's where automation comes in. In this guide, we'll build a Python application that monitors product prices across any e-commerce website and instantly notifies you when prices drop on items you're actually interested in. Here is a sneak peek of the app: + +![Screenshot of a minimalist price tracking application showing product listings, price history charts, and notification controls for monitoring e-commerce deals using Firecrawl](images/sneak-peek.png) + +The app has a simple appearance but provides complete functionality: + +- It has a minimalistic UI to add or remove products from the tracker +- A simple dashboard to display price history for each product +- Controls for setting the price drop threshold in percentages +- A notification system that sends Discord alerts when a tracked item's price drops +- A scheduling system that updates the product prices on an interval you specify +- Runs for free for as long as you want + +Even though the title says "Amazon price tracker" (full disclosure: I was forced to write that for SEO purposes), the app will work for any e-commerce website you can imagine (except Ebay, for some reason). + +So, let's get started building this Amazon price tracker. + +## The Toolstack We Will Use + +The app will be built using Python and these libraries:: + +- [Streamlit](streamlit.io) for the UI +- [Firecrawl](firecrawl.dev) for AI-based scraping of e-commerce websites +- [SQLAlchemy](https://www.sqlalchemy.org/) for database management + +In addition to Python, we will use these platforms: + +- Discord for notifications +- GitHub for hosting the app +- GitHub Actions for running the app on a schedule +- Supabase for hosting a free Postgres database instance + +## Building an Amazon Price Tracker App Step-by-step + +Since this project involves multiple components working together, we'll take a top-down approach rather than building individual pieces first. This approach makes it easier to understand how everything fits together, since we'll introduce each tool only when it's needed. The benefits of this strategy will become clear as we progress through the tutorial. + +### Step 1: Setting up the environment + +First, let's create a dedicated environment on our machines to work on the project: + +```bash +mkdir automated-price-tracker +cd automated-price-tracker +python -m venv .venv +source .venv/bin/activate +``` + +These commands create a working directory and activate a virtual environment. Next, create a new script called `ui.py` for designing the user interface with Streamlit. + +```bash +touch ui.py +``` + +Then, install Streamlit: + +```bash +pip install streamlit +``` + +Next, create a `requirements.txt` file and add Streamlit as the first dependency: + +```bash +touch requirements.txt +echo "streamlit\n" >> requirements.txt +``` + +Since the code will be hosted on GitHub, we need to initialize Git and create a `.gitignore` file: + +```bash +git init +touch .gitignore +echo ".venv" >> .gitignore # Add the virtual env folder +git commit -m "Initial commit" +``` + +### Step 2: Add a sidebar to the UI for product input + +Let's take a look at the final product one more time: + +![A screenshot of an Amazon price tracker web application showing a sidebar for adding product URLs and a main dashboard displaying tracked products with price history charts. Created with streamlit and firecrawl](images/sneak-peek.png) + +It has two sections: the sidebar and the main dashboard. Since the first thing you do when launching this app is adding products, we will start building the sidebar first. Open `ui.py` and paste the following code: + +```python +import streamlit as st + +# Set up sidebar +with st.sidebar: + st.title("Add New Product") + product_url = st.text_input("Product URL") + add_button = st.button("Add Product") + +# Main content +st.title("Price Tracker Dashboard") +st.markdown("## Tracked Products") +``` + +The code snippet above sets up a basic Streamlit web application with two main sections. In the sidebar, it creates a form for adding new products with a text input field for the product URL and an "Add Product" button. The main content area contains a dashboard title and a section header for tracked products. The code uses Streamlit's `st.sidebar` context manager to create the sidebar layout and basic Streamlit components like `st.title`, `st.text_input`, and `st.button` to build the user interface elements. + +To see how this app looks like, run the following command: + +```bash +streamlit run ui.py +``` + +Now, let's add a commit to save our progress: + +```bash +git add . +git commit -m "Add a sidebar to the basic UI" +``` + +### Step 3: Add a feature to check if input URL is valid + +In the next step, we want to add some restrictions to the input field like checking if the passed URL is valid. For this, create a new file called `utils.py` where we write additional utility functions for our app: + +```bash +touch utils.py +``` + +Inside the script, paste following code: + +```bash +# utils.py +from urllib.parse import urlparse +import re + + +def is_valid_url(url: str) -> bool: + try: + # Parse the URL + result = urlparse(url) + + # Check if scheme and netloc are present + if not all([result.scheme, result.netloc]): + return False + + # Check if scheme is http or https + if result.scheme not in ["http", "https"]: + return False + + # Basic regex pattern for domain validation + domain_pattern = ( + r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$" + ) + if not re.match(domain_pattern, result.netloc): + return False + + return True + + except Exception: + return False +``` + +The above function `is_valid_url()` validates URLs by checking several criteria: + +1. It verifies the URL has both a scheme (`http`/`https`) and domain name +2. It ensures the scheme is specifically `http` or `https` +3. It validates the domain name format using regex to check for valid characters and TLD +4. It returns True only if all checks pass, False otherwise + +Let's use this function in our `ui.py` file. Here is the modified code: + +```python +import streamlit as st +from utils import is_valid_url + + +# Set up sidebar +with st.sidebar: + st.title("Add New Product") + product_url = st.text_input("Product URL") + add_button = st.button("Add Product") + + if add_button: + if not product_url: + st.error("Please enter a product URL") + elif not is_valid_url(product_url): + st.error("Please enter a valid URL") + else: + st.success("Product is now being tracked!") + +# Main content +... +``` + +Here is what's new: + +1. We added URL validation using the `is_valid_url()` function from `utils.py` +2. When the button is clicked, we perform validation: + - Check if URL is empty + - Validate URL format using `is_valid_url()` +3. User feedback is provided through error/success messages: + - Error shown for empty URL + - Error shown for invalid URL format + - Success message when URL passes validation + +Rerun the Streamlit app again and see if our validation works. Then, return to your terminal to commit the changes we've made: + +```bash +git add . +git commit -m "Add a feature to check URL validity" +``` + +### Step 4: Scrape the input URL for product details + +When a valid URL is entered and the add button is clicked, we need to implement product scraping functionality instead of just showing a success message. The system should: + +1. Immediately scrape the product URL to extract key details: + - Product name + - Current price + - Main product image + - Brand name + - Other relevant attributes + +2. Store these details in a database to enable: + - Regular price monitoring + - Historical price tracking + - Price change alerts + - Product status updates + +For the scraper, we will use [Firecrawl](firecrawl.dev), an AI-based scraping API for extracting webpage data without HTML parsing. This solution provides several advantages: + +1. No website HTML code analysis required for element selection +2. Resilient to HTML structure changes through AI-based element detection +3. Universal compatibility with product webpages due to structure-agnostic approach +4. Reliable website blocker bypass via robust API infrastructure + +First, create a new file called `scraper.py`: + +```bash +touch scraper.py +``` + +Then, install these three libraries: + +```bash +pip install firecrawl-py pydantic python-dotenv +echo "firecrawl-py\npydantic\npython-dotenv\n" >> requirements.txt # Add them to dependencies +``` + +`firecrawl-py` is the Python SDK for Firecrawl scraping engine, `pydantic` is a data validation library that helps enforce data types and structure through Python class definitions, and `python-dotenv` is a library that loads environment variables from a `.env` file into your Python application. + +With that said, head over to the Firecrawl website and [sign up for a free account](https://www.firecrawl.dev/) (the free plan will work fine). You will be given an API key, which you should copy. + +Then, create a `.env` file in your terminal and add the API key as an environment variable: + +```bash +touch .env +echo "FIRECRAWL_API_KEY='YOUR-API-KEY-HERE' >> .env" +echo ".env" >> .gitignore # Ignore .env files in Git +``` + +The `.env` file is used to securely store sensitive configuration values like API keys that shouldn't be committed to version control. By storing the Firecrawl API key in `.env` and adding it to `.gitignore`, we ensure it stays private while still being accessible to our application code. This is a security best practice to avoid exposing credentials in source control. + +Now, we can start writing the `scraper.py`: + +```python +from firecrawl import FirecrawlApp +from pydantic import BaseModel, Field +from dotenv import load_dotenv +from datetime import datetime + +load_dotenv() + +app = FirecrawlApp() +``` + +Here, `load_dotenv()` function reads the `.env` file you have in your working directory and loads the environment variables inside, including the Firecrawl API key. When you create an instance of `FirecrawlApp` class, the API key is automatically detected to establish a connection between your script and the scraping engine in the form of the `app` variable. + +Now, we create a Pydantic class (usually called a model) that defines the details we want to scrape from each product: + +```python +class Product(BaseModel): + """Schema for creating a new product""" + + url: str = Field(description="The URL of the product") + name: str = Field(description="The product name/title") + price: float = Field(description="The current price of the product") + currency: str = Field(description="Currency code (USD, EUR, etc)") + main_image_url: str = Field(description="The URL of the main image of the product") +``` + +Pydantic models may be completely new to you, so let's break down the `Product` model: + +- The `url` field stores the product page URL we want to track +- The `name` field stores the product title/name that will be scraped +- The `price` field stores the current price as a float number +- The `currency` field stores the 3-letter currency code (e.g. USD, EUR) +- The `main_image_url` field stores the URL of the product's main image + +Each field is typed and has a description that documents its purpose. The `Field` class from Pydantic allows us to add metadata like descriptions to each field. These descriptions are especially important for Firecrawl since it uses them to automatically locate the relevant HTML elements containing the data we want. + +Now, let's create a function to call the engine to scrape URL's based on the schema above: + +```python +def scrape_product(url: str): + extracted_data = app.scrape_url( + url, + params={ + "formats": ["extract"], + "extract": {"schema": Product.model_json_schema()}, + }, + ) + + # Add the scraping date to the extracted data + extracted_data["extract"]["timestamp"] = datetime.utcnow() + + return extracted_data["extract"] + + +if __name__ == "__main__": + product = "https://www.amazon.com/gp/product/B002U21ZZK/" + + print(scrape_product(product)) +``` + +The code above defines a function called `scrape_product` that takes a URL as input and uses it to scrape product information. Here's how it works: + +The function calls `app.scrape_url` with two parameters: + +1. The product URL to scrape +2. A params dictionary that configures the scraping: + - It specifies we want to use the "extract" format + - It provides our `Product` Pydantic model schema as the extraction template as a JSON object + +The scraper will attempt to find and extract data that matches our Product schema fields - the URL, name, price, currency, and image URL. + +The function returns just the "extract" portion of the scraped data, which contains the structured product information. `extract` returns a dictionary to which we add the date of the scraping as it will be important later on. + +Let's test the script by running it: + +```bash +python scraper.py +``` + +You should get an output like this: + +```python +{ + 'url': 'https://www.amazon.com/dp/B002U21ZZK', + 'name': 'MOVA Globe Earth with Clouds 4.5"', + 'price': 212, + 'currency': 'USD', + 'main_image_url': 'https://m.media-amazon.com/images/I/41bQ3Y58y3L._AC_.jpg', + 'timestamp': '2024-12-05 13-20' +} +``` + +The output shows that a [MOVA Globe](https://www.amazon.com/dp/B002U21ZZK) costs $212 USD on Amazon at the time of writing this article. You can test the script for any other website that contains the information we are looking (except Ebay): + +- Price +- Product name/title +- Main image URL + +One key advantage of using Firecrawl is that it returns data in a consistent dictionary format across all websites. Unlike HTML-based scrapers like BeautifulSoup or Scrapy which require custom code for each site and can break when website layouts change, Firecrawl uses AI to understand and extract the requested data fields regardless of the underlying HTML structure. + +Finish this step by committing the new changes to Git: + +```bash +git add . +git commit -m "Implement a Firecrawl scraper for products" +``` + +### Step 5: Storing new products in a PostgreSQL database + +If we want to check product prices regularly, we need to have an online database. In this case, Postgres is the best option since it's reliable, scalable, and has great support for storing time-series data like price histories. + +There are many platforms for hosting Postgres instances but the one I find the easiest and fastest to set up is Supabase. So, please head over to [the Supabase website](https://supabase.com) and create your free account. During the sign-up process, you will be given a password, which you should save somewhere safe on your machine. + +Then, in a few minutes, your free Postgres instance comes online. To connect to this instance, click on Home in the left sidebar and then, "Connect": + +![Screenshot of Supabase dashboard showing database connection settings and credentials for connecting to a PostgreSQL database instance](images/supabase_connect.png) + +You will be shown your database connection string with a placeholder for the password you copied. You should paste this string in your `.env` file with your password added to the `.env` file: + +```bash +echo POSTGRES_URL="THE-SUPABASE-URL-STRING-WITH-YOUR-PASSWORD-ADDED" +``` + +Now, the easiest way to interact with this database is through SQLAlchemy. Let's install it: + +```bash +pip install "sqlalchemy==2.0.35" psycopg2-binary +echo "psycopg2-binary\nsqlalchemy==2.0.35\n" >> requirements.txt +``` + +> Note: [SQLAlchemy](https://sqlalchemy.org) is a Python SQL toolkit and Object-Relational Mapping (ORM) library that lets us interact with databases using Python code instead of raw SQL. For our price tracking project, it provides essential features like database connection management, schema definition through Python classes, and efficient querying capabilities. This makes it much easier to store and retrieve product information and price histories in our Postgres database. + +After the installation, create a new `database.py` file for storing database-related functions: + +```bash +touch database.py +``` + +Let's populate this script: + +```python +from sqlalchemy import create_engine, Column, String, Float, DateTime, ForeignKey +from sqlalchemy.orm import sessionmaker, relationship, declarative_base +from datetime import datetime + +Base = declarative_base() + + +class Product(Base): + __tablename__ = "products" + + url = Column(String, primary_key=True) + prices = relationship( + "PriceHistory", back_populates="product", cascade="all, delete-orphan" + ) + + +class PriceHistory(Base): + __tablename__ = "price_histories" + + id = Column(String, primary_key=True) + product_url = Column(String, ForeignKey("products.url")) + name = Column(String, nullable=False) + price = Column(Float, nullable=False) + currency = Column(String, nullable=False) + main_image_url = Column(String) + timestamp = Column(DateTime, nullable=False) + product = relationship("Product", back_populates="prices") + +``` + +The code above defines two SQLAlchemy models for our price tracking database: + +The `Product` model acts as a registry of all items we want to track. It's kept simple with just the URL as we don't want to duplicate data that changes over time. + +The `PriceHistory` model stores the actual price data points and product details at specific moments in time. This separation allows us to: + +- Track how product details (name, price, image) change over time +- Maintain a clean historical record for each product +- Efficiently query price trends without loading unnecessary data + +Each record in `PriceHistory` contains: + +- A unique ID as primary key +- The product URL as a foreign key linking to the `Product` +- The product name +- The price value and currency +- The main product image URL +- A timestamp of when the price was recorded + +The relationship between `Product` and `PriceHistory` is bidirectional, allowing easy navigation between related records. The `cascade` setting ensures price histories are deleted when their product is deleted. + +These models provide the structure for storing and querying our price tracking data in a PostgreSQL database using SQLAlchemy's ORM capabilities. + +Now, we define a `Database` class with a singe `add_product` method: + +```python +class Database: + def __init__(self, connection_string): + self.engine = create_engine(connection_string) + Base.metadata.create_all(self.engine) + self.Session = sessionmaker(bind=self.engine) + + def add_product(self, url): + session = self.Session() + try: + # Create the product entry + product = Product(url=url) + session.merge(product) # merge will update if exists, insert if not + session.commit() + finally: + session.close() +``` + +The `Database` class above provides core functionality for managing product data in our PostgreSQL database. It takes a connection string in its constructor to establish the database connection using SQLAlchemy. + +The `add_product` method allows us to store new product URLs in the database. It uses SQLAlchemy's `merge` functionality which intelligently handles both inserting new products and updating existing ones, preventing duplicate entries. + +The method carefully manages database sessions, ensuring proper resource cleanup by using `try`/`finally` blocks. This prevents resource leaks and maintains database connection stability. + +Let's use this method inside the sidebar of our UI. Switch to `ui.py` and make the following adjustments: + +First, update the imports to load the Database class and initialize it: + +```python +import os +import streamlit as st + +from utils import is_valid_url +from database import Database +from dotenv import load_dotenv + +load_dotenv() + +with st.spinner("Loading database..."): + db = Database(os.getenv("POSTGRES_URL")) +``` + +The code integrates the `Database` class into the Streamlit UI by importing required dependencies and establishing a database connection. The database URL is loaded securely from environment variables using `python-dotenv`. The `Database` class creates or updates the tables we specified in `database.py` after being initialized. + +The database initialization process is wrapped in a Streamlit spinner component to maintain responsiveness while establishing the connection. This provides visual feedback during the connection setup period, which typically requires a brief initialization time. + +Then, in the sidebar code, we only need to add a single line of code to add the product to the database if the URL is valid: + +```python +# Set up sidebar +with st.sidebar: + st.title("Add New Product") + product_url = st.text_input("Product URL") + add_button = st.button("Add Product") + + if add_button: + if not product_url: + st.error("Please enter a product URL") + elif not is_valid_url(product_url): + st.error("Please enter a valid URL") + else: + db.add_product(product_url) # This is the new line + st.success("Product is now being tracked!") +``` + +In the final `else` block that runs when the product URL is valid, we call the `add_product` method to store the product in the database. + +Let's commit everything: + +```bash +git add . +git commit -m "Add a Postgres database integration for tracking product URLs" +``` + +### Step 6: Storing price histories for new products + +Now, after the product is added to the `products` table, we want to add its details and its scraped price to the `price_histories` table. + +First, switch to `database.py` and add a new method for creating entries in the `PriceHistories` table: + +```python +class Database: + ... # the rest of the class + + def add_price(self, product_data): + session = self.Session() + try: + price_history = PriceHistory( + id=f"{product_data['url']}_{product_data['timestamp']}", + product_url=product_data["url"], + name=product_data["name"], + price=product_data["price"], + currency=product_data["currency"], + main_image_url=product_data["main_image_url"], + timestamp=product_data["timestamp"], + ) + session.add(price_history) + session.commit() + finally: + session.close() +``` + +The `add_price` method takes a dictionary containing product data (which is returned by our scraper) and creates a new entry in the `PriceHistory` table. The entry's ID is generated by combining the product URL with a timestamp. The method stores essential product information like name, price, currency, image URL, and the timestamp of when the price was recorded. It uses SQLAlchemy's session management to safely commit the new price history entry to the database. + +Now, we need to add this functionality to the sidebar as well. In `ui.py`, add a new import statement that loads the `scrape_product` function from `scraper.py`: + +```python +... # The rest of the imports +from scraper import scrape_product +``` + +Then, update the `else` block in the sidebar again: + +```python +with st.sidebar: + st.title("Add New Product") + product_url = st.text_input("Product URL") + add_button = st.button("Add Product") + + if add_button: + if not product_url: + st.error("Please enter a product URL") + elif not is_valid_url(product_url): + st.error("Please enter a valid URL") + else: + db.add_product(product_url) + with st.spinner("Added product to database. Scraping product data..."): + product_data = scrape_product(product_url) + db.add_price(product_data) + st.success("Product is now being tracked!") +``` + +Now when a user enters a product URL and clicks the "Add Product" button, several things happen: + +1. The URL is validated to ensure it's not empty and is properly formatted. +2. If valid, the URL is added to the products table via `add_product()`. +3. The product page is scraped immediately to get current price data. +4. This initial price data is stored in the price history table via `add_price()`. +5. The user sees loading spinners and success messages throughout the process. + +This gives us a complete workflow for adding new products to track, including capturing their initial price point. The UI provides clear feedback at each step and handles errors gracefully. + +Check that everything is working the way we want it and then, commit the new changes: + +```bash +git add . +git commit -m "Add a feature to track product prices after they are added" +``` + +### Step 7: Displaying each product's price history in the main dashboard + +Let's take a look at the final product shown in the introduction once again: + +![Screenshot of a minimalist price tracking dashboard showing product price history charts, add/remove product controls, and notification settings for monitoring e-commerce deals and price drops](images/sneak-peek.png) + +Apart from the sidebar, the main dashboard shows each product's price history visualized with a Plotly line plot where the X axis is the timestamp while the Y axis is the prices. Each line plot is wrapped in a Streamlit component that includes buttons for removing the product from the database or visiting its source URL. + +In this step, we will implement the plotting feature and leave the two buttons for a later section. First, add a new method to the `Database` class for retrieving the price history for each product: + +```python +class Database: + ... # The rest of the code + + def get_price_history(self, url): + """Get price history for a product""" + session = self.Session() + try: + return ( + session.query(PriceHistory) + .filter(PriceHistory.product_url == url) + .order_by(PriceHistory.timestamp.desc()) + .all() + ) + finally: + session.close() +``` + +The method queries the price histories table based on product URL, orders the rows in descending order (oldest first) and returns the results. + +Then, add another method for retrieving all products from the `products` table: + +```python +class Database: + ... + + def get_all_products(self): + session = self.Session() + try: + return session.query(Product).all() + finally: + session.close() +``` + +The idea is that every time our Streamlit app is opened, the main dashboard queries all existing products from the database and render their price histories with line charts in dedicated components. + +To create the line charts, we need Plotly and Pandas, so install them in your environment: + +```bash +pip install pandas plotly +echo "pandas\nplotly\n" >> requirements.txt +``` + +Afterward, import them at the top of `ui.py` along with other existing imports: + +```python +import pandas as pd +import plotly.express as px +``` + +Then, switch to `ui.py` and paste the following snippet of code after the Main content section: + +```python +# Main content +st.title("Price Tracker Dashboard") +st.markdown("## Tracked Products") + +# Get all products +products = db.get_all_products() +``` + +Here, after the page title and subtitle is shown, we are retrieving all products from the database. Let's loop over them: + +```python +# Create a card for each product +for product in products: + price_history = db.get_price_history(product.url) + if price_history: + # Create DataFrame for plotting + df = pd.DataFrame( + [ + {"timestamp": ph.timestamp, "price": ph.price, "name": ph.name} + for ph in price_history + ] + ) +``` + +For each product, we get their price history with `db.get_price_history` and then, convert this data into a dataframe with three columns: + +- Timestamp +- Price +- Product name + +This makes plotting easier with Plotly. Next, we create a Streamlit expander component for each product: + +```python +# Create a card for each product +for product in products: + price_history = db.get_price_history(product.url) + if price_history: + ... + # Create a card-like container for each product + with st.expander(df["name"][0], expanded=False): + st.markdown("---") + col1, col2 = st.columns([1, 3]) + + with col1: + if price_history[0].main_image_url: + st.image(price_history[0].main_image_url, width=200) + st.metric( + label="Current Price", + value=f"{price_history[0].price} {price_history[0].currency}", + ) +``` + +The expander shows the product name as its title and contains: + +1. A divider line +2. Two columns: + - Left column: Product image (if available) and current price metric + - Right column (shown in next section) + +The price is displayed using Streamlit's metric component which shows the current price and currency. + +Here is the rest of the code: + +```python + ... + + with col2: + # Create price history plot + fig = px.line( + df, + x="timestamp", + y="price", + title=None, + ) + fig.update_layout( + xaxis_title=None, + yaxis_title="Price", + showlegend=False, + margin=dict(l=0, r=0, t=0, b=0), + height=300, + ) + fig.update_xaxes(tickformat="%Y-%m-%d %H:%M", tickangle=45) + fig.update_yaxes(tickprefix=f"{price_history[0].currency} ", tickformat=".2f") + st.plotly_chart(fig, use_container_width=True) +``` + +In the right column, we create an interactive line plot using Plotly Express to visualize the price history over time. The plot shows price on the y-axis and timestamp on the x-axis. The layout is customized to remove the title, adjust axis labels and formatting, and optimize the display size. The timestamps are formatted to show date and time, with angled labels for better readability. Prices are displayed with 2 decimal places and a dollar sign prefix. The plot is rendered using Streamlit's `plotly_chart` component and automatically adjusts its width to fill the container. + +After this step, the UI must be fully functional and ready to track products. For example, here is what mine looks like after adding a couple of products: + +![Screenshot of a price tracking dashboard showing multiple product listings with price history charts, product images, and current prices for Amazon items](images/finished.png) + +But notice how the price history chart doesn't show anything. That's because we haven't populated it by checking the product price in regular intervals. Let's do that in the next couple of steps. For now, commit the latest changes we've made: + +```bash +git add . +git commit -m "Display product price histories for each product in the dashboard" +``` + +------------ + +Let's take a brief moment to summarize the steps we took so far and what's next. So far, we've built a Streamlit interface that allows users to add product URLs and displays their current prices and basic information. We've implemented the database schema, created functions to scrape product data, and designed a clean UI with price history visualization. The next step is to set up automated price checking to populate our history charts and enable proper price tracking over time. + +### Step 8: Adding new price entries for existing products + +Now, we want to write a script that adds new price entries in the `price_histories` table for each product in `products` table. We call this script `check_prices.py`: + +```python +import os +from database import Database +from dotenv import load_dotenv +from firecrawl import FirecrawlApp +from scraper import scrape_product + +load_dotenv() + +db = Database(os.getenv("POSTGRES_URL")) +app = FirecrawlApp() +``` + +At the top, we are importing the functions and packages and initializing the database and a Firecrawl app. Then, we define a simple `check_prices` function: + +```python +def check_prices(): + products = db.get_all_products() + + for product in products: + try: + updated_product = scrape_product(product.url) + db.add_price(updated_product) + print(f"Added new price entry for {updated_product['name']}") + except Exception as e: + print(f"Error processing {product.url}: {e}") + + +if __name__ == "__main__": + check_prices() +``` + +In the function body, we retrieve all products URLs, retrieve their new price data with `scrape_product` function from `scraper.py` and then, add a new price entry for the product with `db.add_price`. + +If you run the function once and refresh the Streamlit app, you must see a line chart appear for each product you are tracking: + +![Screenshot of a price tracking dashboard showing a line chart visualization of product price history over time, with price on the y-axis and dates on the x-axis](images/linechart.png) + +Let's commit the changes in this step: + +```bash +git add . +git commit -m "Add a script for checking prices of existing products" +``` + +### Step 9: Check prices regularly with GitHub actions + +GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate various software workflows directly from your GitHub repository. In our case, it's particularly useful because we can set up automated price checks to run the `check_prices.py` script at regular intervals (e.g., daily or hourly) without manual intervention. This ensures we consistently track price changes and maintain an up-to-date database of historical prices for our tracked products. + +So, the first step is creating a new GitHub repository for our project and pushing existing code to it: + +```bash +git remote add origin https://github.com/yourusername/price-tracker.git +git push origin main +``` + +Then, return to your terminal and create this directory structure: + +```bash +mkdir -p .github/workflows +touch .github/workflows/check_prices.yml +``` + +The first command creates a new directory structure `.github/workflows` using the `-p` flag to create parent directories if they don't exist. + +The second command creates an empty YAML file called `check_prices.yml` inside the workflows directory. GitHub Actions looks for workflow files in this specific location - any YAML files in the `.github/workflows` directory will be automatically detected and processed as workflow configurations. These YAML files define when and how your automated tasks should run, what environment they need, and what commands to execute. In our case, this file will contain instructions for GitHub Actions to periodically run our price checking script. Let's write it: + +```yaml +name: Price Check + +on: + schedule: + # Runs every 3 minutes + - cron: "*/3 * * * *" + workflow_dispatch: # Allows manual triggering +``` + +Let's break down this first part of the YAML file: + +The `name: Price Check` line gives our workflow a descriptive name that will appear in the GitHub Actions interface. + +The `on:` section defines when this workflow should be triggered. We've configured two triggers: + +1. A schedule using cron syntax `*/3 * * * *` which runs the workflow every 3 minutes. The five asterisks represent minute, hour, day of month, month, and day of week respectively. The `*/3` means "every 3rd minute". The 3-minute interval is for debugging purposes, we will need to choose a wider interval later on to respect the free limits of GitHub actions. + +2. `workflow_dispatch` enables manual triggering of the workflow through the GitHub Actions UI, which is useful for testing or running the check on-demand. + +Now, let's add the rest: + +```yaml +jobs: + check-prices: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run price checker + env: + FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} + POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + run: python check_prices.py +``` + +Let's break down this second part of the YAML file: + +The `jobs:` section defines the actual work to be performed. We have one job named `check-prices` that runs on an Ubuntu virtual machine (`runs-on: ubuntu-latest`). + +Under `steps:`, we define the sequence of actions: + +1. First, we checkout our repository code using the standard `actions/checkout@v4` action + +2. Then we set up Python 3.10 using `actions/setup-python@v5`, enabling pip caching to speed up dependency installation + +3. Next, we install our Python dependencies by upgrading `pip` and installing requirements from our `requirements.txt` file. At this point, it is essential that you were keeping a complete dependency file based on the installs we made in the project. + +4. Finally, we run our price checker script, providing two environment variables: + - `FIRECRAWL_API_KEY`: For accessing the web scraping service + - `POSTGRES_URL`: For connecting to our database + +Both variables must be stored in our GitHub repository as secrets for this workflow file to run without errors. So, navigate to the repository you've created for the project and open its Settings. Under "Secrets and variables" > "Actions", click on "New repository secret" button to add the environment variables we have in the `.env` file one-by-one. + +Then, return to your terminal, commit the changes and push: + +```bash +git add . +git commit -m "Add a workflow to check prices regularly" +git push origin main +``` + +Next, navigate to your GitHub repository again and click on the "Actions" tab: + +![Screenshot of GitHub Actions interface showing workflow runs and manual trigger button for automated price tracking application](images/actions.png) + +From there, you can run the workflow manually (click "Run workflow" and refresh the page). If it is executed successfully, you can return to the Streamlit app and refresh to see the new price added to the chart. + +### Step 10: Setting up Discord for notifications + +Now that we know our scheduling workflow works, the first order of business is setting a wider check interval in the workflow file. Even though our first workflow run was manually, the rest happen automatically. + +```bash +on: + schedule: + # Runs every 6 hours + - cron: "0 0,6,12,18 * * *" + workflow_dispatch: # Allows manual triggering +``` + +The cron syntax `0 0,6,12,18 * * *` can be broken down as follows: + +- First `0`: Run at minute 0 +- `0,6,12,18`: Run at hours 0 (midnight), 6 AM, 12 PM (noon), and 6 PM +- First `*`: Run every day of the month +- Second `*`: Run every month +- Third `*`: Run every day of the week + +So this schedule will check prices four times daily: at midnight, 6 AM, noon, and 6 PM (UTC time). This spacing helps stay within GitHub Actions' free tier limits while still catching most price changes. + +Now, commit and push the changes: + +```bash +git add . +git commit -m "Set a wider check interval in the workflow file" +git push origin main +``` + +Now comes the interesting part. Each time the workflow is run, we want to compare the current price of the product to its original price when we started tracking it. If the difference between these two prices exceeds a certain threshold like 5%, this means there is a discount happening for the product and we want to send a notification. + +The easiest way to set this up is by using Discord webhooks. So, if you don't have one already, go to Discord.com and create a new account (optionally, download the desktop app as well). Then, setting up Discord notifications requires a few careful steps: + +1. **Create a discord server** + - Click the "+" button in the bottom-left corner of Discord + - Choose "Create My Own" → "For me and my friends" + - Give your server a name (e.g., "Price Alerts") + +2. **Create a channel for alerts** + - Your server comes with a #general channel by default + - You can use this or create a new channel called #price-alerts + - Right-click the channel you want to use + +3. **Set up the webhook** + - Select "Edit Channel" from the right-click menu + - Go to the "Integrations" tab + - Click "Create Webhook" + - Give it a name like "Price Alert Bot" + - The webhook URL will be generated automatically + - Click "Copy Webhook URL" - this is your unique notification endpoint + +4. **Secure the webhook URL** + - Never share or commit your webhook URL directly + - Add it to your `.env` file as `DISCORD_WEBHOOK_URL` + - Add it to your GitHub repository secrets + - The URL should look something like: `https://discord.com/api/webhooks/...` + +This webhook will serve as a secure endpoint that our price tracker can use to send notifications directly to your Discord channel. + +Webhooks are automated messages sent from apps to other apps in real-time. They work like a notification system - when something happens in one app, it automatically sends data to another app through a unique URL. In our case, we'll use Discord webhooks to automatically notify us when there's a price drop. Whenever our price tracking script detects a significant discount, it will send a message to our Discord channel through the webhook URL, ensuring we never miss a good deal. + +After copying the webhook URL, you should save it as environment variable to your `.env` file: + +```python +echo "DISCORD_WEBHOOK_URL='THE-URL-YOU-COPIED'" >> .env +``` + +Now, create a new file called `notifications.py` and paste the following contents: + +```python +from dotenv import load_dotenv +import os +import aiohttp +import asyncio + +load_dotenv() + + +async def send_price_alert( + product_name: str, old_price: float, new_price: float, url: str +): + """Send a price drop alert to Discord""" + drop_percentage = ((old_price - new_price) / old_price) * 100 + + message = { + "embeds": [ + { + "title": "Price Drop Alert! 🎉", + "description": f"**{product_name}**\nPrice dropped by {drop_percentage:.1f}%!\n" + f"Old price: ${old_price:.2f}\n" + f"New price: ${new_price:.2f}\n" + f"[View Product]({url})", + "color": 3066993, + } + ] + } + + try: + async with aiohttp.ClientSession() as session: + await session.post(os.getenv("DISCORD_WEBHOOK_URL"), json=message) + except Exception as e: + print(f"Error sending Discord notification: {e}") +``` + +The `send_price_alert` function above is responsible for sending price drop notifications to Discord using webhooks. Let's break down what's new: + +1. The function takes 4 parameters: + - `product_name`: The name of the product that dropped in price + - `old_price`: The previous price before the drop + - `new_price`: The current lower price + - `url`: Link to view the product + +2. It calculates the percentage drop in price using the formula: `((old_price - new_price) / old_price) * 100` + +3. The notification is formatted as a Discord embed - a rich message format that includes: + - A title with a celebration emoji + - A description showing the product name, price drop percentage, old and new prices + - A link to view the product + - A green color (3066993 in decimal) + +4. The message is sent asynchronously using `aiohttp` to post to the Discord webhook URL stored in the environment variables + +5. Error handling is included to catch and print any issues that occur during the HTTP request + +This provides a clean way to notify users through Discord whenever we detect a price drop for tracked products. + +To check the notification system works, add this main block to the end of the script: + +```python +if __name__ == "__main__": + asyncio.run(send_price_alert("Test Product", 100, 90, "https://www.google.com")) +``` + +`asyncio.run()` is used here because `send_price_alert` is an async function that needs to be executed in an event loop. `asyncio.run()` creates and manages this event loop, allowing the async HTTP request to be made properly. Without it, we wouldn't be able to use the `await` keyword inside `send_price_alert`. + +To run the script, install `aiohttp`: + +```python +pip install aiohttp +echo "aiohttp\n" >> requirements.txt +python notifications.py +``` + +If all is well, you should get a Discord message in your server that looks like this: + +![Screenshot of a Discord notification showing a price drop alert with product details, original price, new discounted price and percentage savings](images/alert.png) + +Let's commit the changes we have: + +```bash +git add . +git commit -m "Set up Discord alert system" +``` + +Also, don't forget to add the Discord webhook URL to your GitHub repository secrets! + +### Step 11: Sending Discord alerts when prices drop + +Now, the only step left is adding a price comparison logic to `check_prices.py`. In other words, we want to use the `send_price_alert` function if the new scraped price is lower than the original. This requires a revamped `check_prices.py` script: + +```python +import os +import asyncio +from database import Database +from dotenv import load_dotenv +from firecrawl import FirecrawlApp +from scraper import scrape_product +from notifications import send_price_alert + +load_dotenv() + +db = Database(os.getenv("POSTGRES_URL")) +app = FirecrawlApp() + +# Threshold percentage for price drop alerts (e.g., 5% = 0.05) +PRICE_DROP_THRESHOLD = 0.05 + + +async def check_prices(): + products = db.get_all_products() + product_urls = set(product.url for product in products) + + for product_url in product_urls: + # Get the price history + price_history = db.get_price_history(product_url) + if not price_history: + continue + + # Get the earliest recorded price + earliest_price = price_history[-1].price + + # Retrieve updated product data + updated_product = scrape_product(product_url) + current_price = updated_product["price"] + + # Add the price to the database + db.add_price(updated_product) + print(f"Added new price entry for {updated_product['name']}") + + # Check if price dropped below threshold + if earliest_price > 0: # Avoid division by zero + price_drop = (earliest_price - current_price) / earliest_price + if price_drop >= PRICE_DROP_THRESHOLD: + await send_price_alert( + updated_product["name"], earliest_price, current_price, product_url + ) + + +if __name__ == "__main__": + asyncio.run(check_prices()) +``` + +Let's examine the key changes in this enhanced version of `check_prices.py`: + +1. New imports and setup + - Added `asyncio` for `async`/`await` support + - Imported `send_price_alert` from `notifications.py` + - Defined `PRICE_DROP_THRESHOLD = 0.05` (5% threshold for alerts) + +2. Async function conversion + - Converted `check_prices()` to async function + - Gets unique product URLs using set comprehension to avoid duplicates + +3. Price history analysis + - Retrieves full price history for each product + - Gets `earliest_price` from `history[-1]` (works because we ordered by timestamp DESC) + - Skips products with no price history using `continue` + +4. Price drop detection logic + - Calculates drop percentage: `(earliest_price - current_price) / earliest_price` + - Checks if drop exceeds 5% threshold + - Sends Discord alert if threshold exceeded using `await send_price_alert()` + +5. Async main block + - Uses `asyncio.run()` to execute async `check_prices()` in event loop + +When I tested this new version of the script, I immediately got an alert: + +![Screenshot of a Discord notification showing a price drop alert for an Amazon product, displaying the original and discounted prices with percentage savings](images/new-alert.png) + +Before we supercharge our workflow with the new notification system, you should add this line of code to your `check_prices.yml` workflow file to read the Discord webhook URL from your GitHub secrets: + +```python +... + - name: Run price checker + env: + FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} + POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: python automated_price_tracking/check_prices.py +``` + +Finally, let's commit everything and push to GitHub so that our workflow is supercharged with our notification system: + +```bash +git add . +git commit -m "Add notification system to price drops" +git push origin main +``` + +## Limitations of Free Tier Tools Used in the Tutorial + +Before wrapping up, let's quickly review the limitations of the free tools we used in this tutorial: + +- GitHub Actions: Limited to 2,000 minutes per month for free accounts. Consider increasing the cron interval to stay within limits. +- Supabase: Free tier includes 500MB database storage and limited row count. Monitor usage if tracking many products. +- Firecrawl: Free API tier allows 500 requests per month. This means that at 6 hour intervals, you can track up to four products in the free plan. +- Streamlit Cloud: Free hosting tier has some memory/compute restrictions and goes to sleep after inactivity. + +While these limitations exist, they're quite generous for personal use and learning. The app will work well for tracking a reasonable number of products with daily price checks. + +## Conclusion and Next Steps + +Congratulations for making it to the end of this extremely long tutorial! We've just covered how to implement an end-to-end Python project you can proudly showcase on your portfolio. We built a complete price tracking system that scrapes product data from e-commerce websites, stores it in a Postgres database, analyzes price histories, and sends automated Discord notifications when prices drop significantly. Along the way, we learned about web scraping with Firecrawl, database management with SQLAlchemy, asynchronous programming with asyncio, building interactive UIs with Streamlit, automating with GitHub actions and integrating external webhooks. + +However, the project is far from perfect. Since we took a top-down approach to building this app, our project code is scattered across multiple files and often doesn't follow programming best practices. For this reason, I've recreated the same project in a much more sophisticated manner with production-level features. [This new version on GitHub](https://github.com/BexTuychiev/automated-price-tracking) implements proper database session management, faster operations and overall smoother user experience. Also, this version includes buttons for removing products from the database and visiting them through the app. + +If you decide to stick with the basic version, you can find the full project code and notebook in the official Firecrawl GitHub repository's example projects. I also recommend that you [deploy your Streamlit app to Streamlit Cloud](https://share.streamlit.io) so that you have a functional app accessible everywhere you go. + +Here are some further improvements you might consider for the app: + +- Improve the price comparison logic: the app compares the current price to the oldest recorded price, which might not be ideal. You may want to compare against recent price trends instead. +- No handling of currency conversion if products use different currencies. +- The Discord notification system doesn't handle rate limits or potential webhook failures gracefully. +- No error handling for Firecrawl scraper - what happens if the scraping fails? +- No consistent usage of logging to help track issues in production. +- No input URL sanitization before scraping. + +Some of these features are implemented in [the advanced version of the project](https://github.com/BexTuychiev/automated-price-tracking), so definitely check it out! + +Here are some more guides from our blog if you are interested: + +- [How to Run Web Scrapers on Schedule](https://www.firecrawl.dev/blog/automated-web-scraping-free-2025) +- [More about using Firecrawl's `scrape_url` function](https://www.firecrawl.dev/blog/mastering-firecrawl-scrape-endpoint) +- [Scraping entire websites with Firecrawl in a single command - the /crawl endpoint](https://www.firecrawl.dev/blog/mastering-the-crawl-endpoint-in-firecrawl) + +Thank you for reading! From 4fc94aba945b147cd64172fcff8d36a8be131b6c Mon Sep 17 00:00:00 2001 From: BexTuychiev Date: Fri, 6 Dec 2024 23:11:35 +0500 Subject: [PATCH 02/52] Add a link to the assets directory that will work once the PR is merged --- examples/blog-articles/amazon-price-tracking/notebook.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/blog-articles/amazon-price-tracking/notebook.md b/examples/blog-articles/amazon-price-tracking/notebook.md index 38744828..01283f2a 100644 --- a/examples/blog-articles/amazon-price-tracking/notebook.md +++ b/examples/blog-articles/amazon-price-tracking/notebook.md @@ -1215,7 +1215,7 @@ Congratulations for making it to the end of this extremely long tutorial! We've However, the project is far from perfect. Since we took a top-down approach to building this app, our project code is scattered across multiple files and often doesn't follow programming best practices. For this reason, I've recreated the same project in a much more sophisticated manner with production-level features. [This new version on GitHub](https://github.com/BexTuychiev/automated-price-tracking) implements proper database session management, faster operations and overall smoother user experience. Also, this version includes buttons for removing products from the database and visiting them through the app. -If you decide to stick with the basic version, you can find the full project code and notebook in the official Firecrawl GitHub repository's example projects. I also recommend that you [deploy your Streamlit app to Streamlit Cloud](https://share.streamlit.io) so that you have a functional app accessible everywhere you go. +If you decide to stick with the basic version, you can find the full project code and notebook in [the official Firecrawl GitHub repository's example projects](https://github.com/mendableai/firecrawl/tree/main/examples/automated_price_tracking). I also recommend that you [deploy your Streamlit app to Streamlit Cloud](https://share.streamlit.io) so that you have a functional app accessible everywhere you go. Here are some further improvements you might consider for the app: From 57ef400473193a9777d2a48f66537251816783f4 Mon Sep 17 00:00:00 2001 From: BexTuychiev Date: Fri, 6 Dec 2024 23:15:12 +0500 Subject: [PATCH 03/52] Add README to auotmated price tracking project --- examples/automated_price_tracking/README.md | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 examples/automated_price_tracking/README.md diff --git a/examples/automated_price_tracking/README.md b/examples/automated_price_tracking/README.md new file mode 100644 index 00000000..9ab50dbe --- /dev/null +++ b/examples/automated_price_tracking/README.md @@ -0,0 +1,31 @@ +# Automated Price Tracking System + +A robust price tracking system that monitors product prices across e-commerce websites and notifies users of price changes through Discord. + +## Features + +- Automated price checking every 6 hours +- Support for multiple e-commerce platforms through Firecrawl API +- Discord notifications for price changes +- Historical price data storage in PostgreSQL database +- Interactive price history visualization with Streamlit + +## Setup + +1. Clone the repository +2. Install dependencies: + + ```bash + pip install -r requirements.txt + ``` + +3. Configure environment variables: + + ```bash + cp .env.example .env + ``` + + Then edit `.env` with your: + - Discord webhook URL + - Database credentials + - Firecrawl API key From a093d55e6dcdd57c964ed377b673762cf9b1c510 Mon Sep 17 00:00:00 2001 From: BexTuychiev Date: Mon, 9 Dec 2024 16:55:04 +0500 Subject: [PATCH 04/52] Add assets for GitHub actions tutorialc --- .../actions.png | Bin .../alert.png | Bin .../discord.png | Bin .../finished.png | Bin .../linechart.png | Bin .../new-alert.png | Bin .../new-server.png | Bin .../sneak-peek.png | Bin .../supabase_connect.png | Bin .../webhook.png | Bin .../amazon-price-tracking/notebook.md | 18 +- .../cron-syntax.png | Bin 0 -> 419696 bytes .../github-actions-tutorial/notebook.ipynb | 1630 +++++++++++++++++ .../github-actions-tutorial/notebook.md | 1187 ++++++++++++ 14 files changed, 2826 insertions(+), 9 deletions(-) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/actions.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/alert.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/discord.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/finished.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/linechart.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/new-alert.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/new-server.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/sneak-peek.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/supabase_connect.png (100%) rename examples/blog-articles/amazon-price-tracking/{images => amazon-price-tracking-images}/webhook.png (100%) create mode 100644 examples/blog-articles/github-actions-tutorial/github-actions-tutorial-images/cron-syntax.png create mode 100644 examples/blog-articles/github-actions-tutorial/notebook.ipynb create mode 100644 examples/blog-articles/github-actions-tutorial/notebook.md diff --git a/examples/blog-articles/amazon-price-tracking/images/actions.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/actions.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/actions.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/actions.png diff --git a/examples/blog-articles/amazon-price-tracking/images/alert.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/alert.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/alert.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/alert.png diff --git a/examples/blog-articles/amazon-price-tracking/images/discord.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/discord.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/discord.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/discord.png diff --git a/examples/blog-articles/amazon-price-tracking/images/finished.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/finished.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/finished.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/finished.png diff --git a/examples/blog-articles/amazon-price-tracking/images/linechart.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/linechart.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/linechart.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/linechart.png diff --git a/examples/blog-articles/amazon-price-tracking/images/new-alert.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/new-alert.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/new-alert.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/new-alert.png diff --git a/examples/blog-articles/amazon-price-tracking/images/new-server.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/new-server.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/new-server.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/new-server.png diff --git a/examples/blog-articles/amazon-price-tracking/images/sneak-peek.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/sneak-peek.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/sneak-peek.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/sneak-peek.png diff --git a/examples/blog-articles/amazon-price-tracking/images/supabase_connect.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/supabase_connect.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/supabase_connect.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/supabase_connect.png diff --git a/examples/blog-articles/amazon-price-tracking/images/webhook.png b/examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/webhook.png similarity index 100% rename from examples/blog-articles/amazon-price-tracking/images/webhook.png rename to examples/blog-articles/amazon-price-tracking/amazon-price-tracking-images/webhook.png diff --git a/examples/blog-articles/amazon-price-tracking/notebook.md b/examples/blog-articles/amazon-price-tracking/notebook.md index 01283f2a..59ca47a5 100644 --- a/examples/blog-articles/amazon-price-tracking/notebook.md +++ b/examples/blog-articles/amazon-price-tracking/notebook.md @@ -19,7 +19,7 @@ The challenge is that e-commerce websites run flash sales and temporary discount That's where automation comes in. In this guide, we'll build a Python application that monitors product prices across any e-commerce website and instantly notifies you when prices drop on items you're actually interested in. Here is a sneak peek of the app: -![Screenshot of a minimalist price tracking application showing product listings, price history charts, and notification controls for monitoring e-commerce deals using Firecrawl](images/sneak-peek.png) +![Screenshot of a minimalist price tracking application showing product listings, price history charts, and notification controls for monitoring e-commerce deals using Firecrawl](amazon-price-tracking-images/sneak-peek.png) The app has a simple appearance but provides complete functionality: @@ -96,7 +96,7 @@ git commit -m "Initial commit" Let's take a look at the final product one more time: -![A screenshot of an Amazon price tracker web application showing a sidebar for adding product URLs and a main dashboard displaying tracked products with price history charts. Created with streamlit and firecrawl](images/sneak-peek.png) +![A screenshot of an Amazon price tracker web application showing a sidebar for adding product URLs and a main dashboard displaying tracked products with price history charts. Created with streamlit and firecrawl](amazon-price-tracking-images/sneak-peek.png) It has two sections: the sidebar and the main dashboard. Since the first thing you do when launching this app is adding products, we will start building the sidebar first. Open `ui.py` and paste the following code: @@ -389,7 +389,7 @@ There are many platforms for hosting Postgres instances but the one I find the e Then, in a few minutes, your free Postgres instance comes online. To connect to this instance, click on Home in the left sidebar and then, "Connect": -![Screenshot of Supabase dashboard showing database connection settings and credentials for connecting to a PostgreSQL database instance](images/supabase_connect.png) +![Screenshot of Supabase dashboard showing database connection settings and credentials for connecting to a PostgreSQL database instance](amazon-price-tracking-images/supabase_connect.png) You will be shown your database connection string with a placeholder for the password you copied. You should paste this string in your `.env` file with your password added to the `.env` file: @@ -623,7 +623,7 @@ git commit -m "Add a feature to track product prices after they are added" Let's take a look at the final product shown in the introduction once again: -![Screenshot of a minimalist price tracking dashboard showing product price history charts, add/remove product controls, and notification settings for monitoring e-commerce deals and price drops](images/sneak-peek.png) +![Screenshot of a minimalist price tracking dashboard showing product price history charts, add/remove product controls, and notification settings for monitoring e-commerce deals and price drops](amazon-price-tracking-images/sneak-peek.png) Apart from the sidebar, the main dashboard shows each product's price history visualized with a Plotly line plot where the X axis is the timestamp while the Y axis is the prices. Each line plot is wrapped in a Streamlit component that includes buttons for removing the product from the database or visiting its source URL. @@ -772,7 +772,7 @@ In the right column, we create an interactive line plot using Plotly Express to After this step, the UI must be fully functional and ready to track products. For example, here is what mine looks like after adding a couple of products: -![Screenshot of a price tracking dashboard showing multiple product listings with price history charts, product images, and current prices for Amazon items](images/finished.png) +![Screenshot of a price tracking dashboard showing multiple product listings with price history charts, product images, and current prices for Amazon items](amazon-price-tracking-images/finished.png) But notice how the price history chart doesn't show anything. That's because we haven't populated it by checking the product price in regular intervals. Let's do that in the next couple of steps. For now, commit the latest changes we've made: @@ -825,7 +825,7 @@ In the function body, we retrieve all products URLs, retrieve their new price da If you run the function once and refresh the Streamlit app, you must see a line chart appear for each product you are tracking: -![Screenshot of a price tracking dashboard showing a line chart visualization of product price history over time, with price on the y-axis and dates on the x-axis](images/linechart.png) +![Screenshot of a price tracking dashboard showing a line chart visualization of product price history over time, with price on the y-axis and dates on the x-axis](amazon-price-tracking-images/linechart.png) Let's commit the changes in this step: @@ -933,7 +933,7 @@ git push origin main Next, navigate to your GitHub repository again and click on the "Actions" tab: -![Screenshot of GitHub Actions interface showing workflow runs and manual trigger button for automated price tracking application](images/actions.png) +![Screenshot of GitHub Actions interface showing workflow runs and manual trigger button for automated price tracking application](amazon-price-tracking-images/actions.png) From there, you can run the workflow manually (click "Run workflow" and refresh the page). If it is executed successfully, you can return to the Streamlit app and refresh to see the new price added to the chart. @@ -1083,7 +1083,7 @@ python notifications.py If all is well, you should get a Discord message in your server that looks like this: -![Screenshot of a Discord notification showing a price drop alert with product details, original price, new discounted price and percentage savings](images/alert.png) +![Screenshot of a Discord notification showing a price drop alert with product details, original price, new discounted price and percentage savings](amazon-price-tracking-images/alert.png) Let's commit the changes we have: @@ -1176,7 +1176,7 @@ Let's examine the key changes in this enhanced version of `check_prices.py`: When I tested this new version of the script, I immediately got an alert: -![Screenshot of a Discord notification showing a price drop alert for an Amazon product, displaying the original and discounted prices with percentage savings](images/new-alert.png) +![Screenshot of a Discord notification showing a price drop alert for an Amazon product, displaying the original and discounted prices with percentage savings](amazon-price-tracking-images/new-alert.png) Before we supercharge our workflow with the new notification system, you should add this line of code to your `check_prices.yml` workflow file to read the Discord webhook URL from your GitHub secrets: diff --git a/examples/blog-articles/github-actions-tutorial/github-actions-tutorial-images/cron-syntax.png b/examples/blog-articles/github-actions-tutorial/github-actions-tutorial-images/cron-syntax.png new file mode 100644 index 0000000000000000000000000000000000000000..d790c1ef66204dc250e07f7c7c7b48dc7b10435b GIT binary patch literal 419696 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%0AjW;?Wd|2H4zrRGJ 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

JgmkS+dOXT+B5o)@mj$waM70>Dyd}C?m7S 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

n0u065Wha1?uJw?0Dj^52n!CAUL{ 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%0AkO!;4 z-EVG2^!$hoFb@<=CYN~3Tfh8Lv1l6lRe@&~!3U89fo@b=EBVpasbl<590p2gh2N*P z(x0x=Z1vduF%($ieqLhS9@>&Y7QJ>RhU?JCAfK?lYs`ZDh2Cg;Dg1RB(=1@%@J`k`%tZUFWoS!WmvHJ$mO)-&PCjgW$A2ms9F0k(=0zDFjZ^JTG zi3z(~Z|j6r2ywk?Zn*tCIfdiwN-BgqA&JEo)Di5rpwt2{6pAJnzrMs_#nsy}VPm{? z3Nykwm!N>aI0}^PUzkH+>9s~`(fX@tD2c&r2g8=DIf)U5=xv#T`=HUEYFprc2=^ng z3Yr~i?f^%MIeZ8KH$OG)>;06hGe7NA7toGva+Ey zxUL$=vVNDb#DkQ*I4sYOh_Zf={_@4ac1n~*{yeV{5(!2CIbM=?6|A_nRo3NGjG?8- z-{)_<@LRBFUtuxkUv=5Wd+-VM&d5n_vys9qbt^HIJ@7VGnV$Bu@Tb%o*C$T7J3pRt zB3qc%ul*+;D*6p`aQ`?y5<>V0M@5Xf~R?4r!SofiNDY+s*ocC zFjz5m-DP%Qll4kUwr4CyUE4XlAd|zIO*}Ig!3hMoW5uz2&lpqbt1gV<%t_BF=@jRn zL3~jVR2Zy0RRVeM4~iQgJoRqeg@P-MPkd@$1wj-MGuizee6ONwE^{EuS$vuW{3cD- zunB=cD~Jj-50z~o^!kDXEt2&*Hgp^6V>~iYm|mcV9m=xvDj+c!OfOdPE9{JLsF?AX z^*d};*>&a}CaCJZ0F1c7XdJaL6horNPKoFWRr@D@4RrR)k-w{5sI_ebI|lB0WP%bV z$m2dgqK;ufSs9Pd$WTDL0){L`g6sv}*{>P?;Df#eZ%rT$4eYgN3m%QVkg4C%ix&#Cg!PJ5&{N6;IPB@_NX0c|w+2xJRe609OoGLS!d$vNubD zxBf4=P{;UD_CWBF0`Re!Ma%Hnw^ki+cR`J+L4VE%OF6Fwgf*hkPp@KF4e+#56pZe) zi2oDept_B4JW&~Y_J>dKM*;e6xhE(6Ottryli9VVvEp4WNz2q0(=sUUlm@`^2by#Y z)nUH7y+VsNXAG4)UX^;)D2A%KXkUvTWb0n%F0c4p%$>x$sA7KT{xQ@iC-LRv@}3Fx z(E!W)&g2z2>H=OBNbtv^y@U->4ux`U3R2j3+U#h9PTY9tp5d_L+TD8yN@vmd3* zBCh|jM>(TpH`4i+TRsaSh`j(KR<8u@5Nh=7QgvBGzE@RJ;X0|Wv#4J7X-b<8pbOjI zX^~C&H2$nAiF5lZcCOLi?tg26hTT2KcEcP&$`e+=^O~A0G@WT3_r0A+y&A#VF9Yhe zKGx3$h96JLMh#tdtq!NcWwec&{+uB}2NdO638JUe(8z1p zo;~kLV83dr+Mt~Q=i0S-lNsLwOyzUzX)6iLggSdZ(`Y%9Ai+uTHi#8PMS{o1?#$el zAd^6MRT$!0CdQrWR@-pt>LJ@1W!_dSbsy;KisEa_ldiB$n{%phTq9Pgqr%ivmN zy(qA#_zU}OEot(WhoK&WnqG0Cj4+t=(nc!WiKGHUm_yg zq=Bp?mX@ubWPn1sh()5H?~zkYLtow%0#E;5tim}be>hd>jZn*z?LwZ&J7frB6v@R3 z|LBvm@woDkHswJ^Uv6Rt^zUrwzaVJ7HK{VfEC_376mTs@Jlx9>_Fqi;$ zk|}pz=LHI3jgZDgjr*e2OVrWX;HeSB?<9%Ai|}HJNhm8 zmlVIeBCh%&P`D(9+#fq2+yR_8=%TY3mz*7MM*-Y>`C*JU_B{qC%oh8^1y^=_)sf%H zmWepfILstsj(+bZ|M(~xU&F0!#Z~-@?tCwdtvuKak+Z*1OdI?;D4txmR2`(`bM2YX z)>F^mXav^Q(a-Mk{{_2?1NQi%6U>o#FDT95fNg^diLQ_hj~ji_Km6AR(G&W7xhI`VA3
5{0vr51D# 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 Xu7IPSE3jFD4183THI&j6O)vi+&?n|< literal 0 HcmV?d00001 diff --git a/examples/blog-articles/github-actions-tutorial/notebook.ipynb b/examples/blog-articles/github-actions-tutorial/notebook.ipynb new file mode 100644 index 00000000..42041b34 --- /dev/null +++ b/examples/blog-articles/github-actions-tutorial/notebook.ipynb @@ -0,0 +1,1630 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comprehensive GitHub Actions Tutorial For Beginners With Examples in Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GitHub Actions is a powerful automation platform that helps developers automate tedious, time-wasting software development workflows. Instead of running tests, executing scripts at intervals, or doing any programmable task manually, you can let GitHub Actions take the wheel when certain events happen in your repository. In this tutorial, you will learn how to use this critical feature of GitHub and design your own workflows for several real-world use cases." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### What are GitHub Actions?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At its core, [GitHub Actions](https://docs.github.com/en/actions) is a continuous integration and continuous delivery (CI/CD) platform that lets you automate various tasks directly from your GitHub repository. Think of it as your personal robot assistant that can:\n", + "\n", + "- Run your Python tests automatically when you push code\n", + "- Deploy your application when you create a new release\n", + "- Send notifications when issues are created\n", + "- Schedule tasks to run at specific times\n", + "- And much more..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Why automate with GitHub Actions?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's look at a common scenario: You are building a Python application that scrapes product prices from various e-commerce websites. Without GitHub actions, you would need to:\n", + "\n", + "1. Manually run your tests after each code change\n", + "2. Remember to execute the scraper at regular intervals\n", + "3. Deploy updates to your production environment\n", + "4. Keep track of environment variables and secrets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "With GitHub actions, all of these tasks can be automated through workflows, usually written in YAML files like below:\n", + "\n", + "```yaml\n", + "name: Run Price Scraper\n", + "\n", + "on:\n", + " schedule:\n", + " - cron: '0 */12 * * *' # Runs every 12 hours\n", + " workflow_dispatch: # Allows manual triggers\n", + "\n", + "jobs:\n", + " scrape:\n", + " runs-on: ubuntu-latest\n", + " \n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: '3.9'\n", + " \n", + " - name: Run scraper\n", + " env:\n", + " API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}\n", + " run: python scraper.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This workflow automatically runs a scraper every 12 hours, handles Python version setup, and securely manages API keys - all without manual intervention." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### What we'll build in this tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Throughout this tutorial, we'll build several practical GitHub Actions workflows for Python applications. You will learn how to:\n", + "\n", + "1. Create basic and advanced workflow configurations.\n", + "2. Work with environment variables and secrets.\n", + "3. Set up automated testing pipelines.\n", + "4. Build a real-world example: an automated scraping system app [Firecrawl](https://firecrawl.dev) in Python.\n", + "5. Implement best practices for security and efficiency. \n", + "\n", + "By the end, you will have hands-on experience with GitHub Actions and be able to automate your own Python projects effectively. \n", + "\n", + "> Note: Even though code examples are Python, the concepts and hands-on experience you will gain from the tutorial will apply to any programming language. \n", + "\n", + "Let's start by understanding the core concepts that make GitHub Actions work." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Understanding GitHub Actions Core Concepts" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To write your own GitHub Actions workflows, you need to understand how its different components work together. Let's break down these core concepts using a practical example: automating tests for a simple Python script." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### GitHub Actions workflows and their components" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A workflow is an automated process that you define in a YAML file within your repository's `.github/workflows` directory. Think of it as a recipe that tells GitHub exactly what to do, how and when to do it. You can transform virtually any programmable task into a GitHub workflow as long as it can be executed in a Linux, Windows, or macOS environment and doesn't require direct user interaction.\n", + "\n", + "Here is a basic workflow structure:\n", + "\n", + "```yaml\n", + "# test.yaml\n", + "name: Python Tests\n", + "on: [push, pull_request]\n", + "\n", + "jobs:\n", + " test:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - name: Check out repository\n", + " uses: actions/checkout@v3\n", + " \n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: '3.9'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The YAML file starts by specifying the name of the workflow with the `name` field. Immediately after, we specify the events that triggers this workflow. In this example, the workflow automatically executes on each `git push` command and pull request. We will learn more about events and triggers in a later section. \n", + "\n", + "Next, we define jobs, which are the building blocks of workflows. Each job:\n", + "\n", + "- Runs on a fresh virtual machine (called a runner) that is specified using the `runs-on` field.\n", + "- Can execute multiple steps in sequence\n", + "- Can run in parallel with other jobs\n", + "- Has access to shared workflow data\n", + "\n", + "For example, you might have separate jobs for testing and deployment:\n", + "\n", + "```yaml\n", + "jobs:\n", + " test:\n", + " runs-on: ubuntu-latest\n", + " ...\n", + " deploy:\n", + " runs-on: macos-latest\n", + " ...\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "Each job can contain one or more `steps` that are executed sequentially. Steps are individual tasks that make up your job. They can:\n", + "\n", + "- Run commands or shell scripts\n", + "- Execute actions (reusable units of code)\n", + "- Run commands in Docker containers\n", + "- Reference other GitHub repositories\n", + "\n", + "For example, a typical test job might have steps to:\n", + "\n", + "1. Check out (clone) code from your GitHub repository\n", + "2. Set up dependencies\n", + "3. Run tests\n", + "4. Upload test results\n", + "\n", + "Each step can specify:\n", + "\n", + "- `name`: A display name for the step\n", + "- `uses`: Reference to an action to run\n", + "- `run`: Any operating-system specific terminal command like `pip install package` or `python script.py`\n", + "- `with`: Input parameters for actions\n", + "- `env`: Environment variables for the step" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we understand jobs and steps, let's look at Actions - the reusable building blocks that make GitHub Actions so powerful.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Actions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `test.yaml` file from earlier has a single `test` job that executes two steps:\n", + "\n", + "1. Checking out the repository code using a built-in `actions/checkout@v3` action.\n", + "2. Setting up a Python environment with `actions/setup-python@v4` and `python-version` as an input parameter for said action.\n", + "\n", + "```bash\n", + "# test.yaml\n", + "name: Python Tests\n", + "on: [push, pull_request]\n", + "\n", + "jobs:\n", + " test:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - name: Check out repository\n", + " uses: actions/checkout@v3\n", + " \n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: '3.9'\n", + "```\n", + "\n", + "Actions are reusable units of code that can be shared across workflows (this is where GitHub Actions take its name). They are like pre-packaged functions that handle common tasks. For instance, instead of writing code to set up Node.js or caching dependencies, you can use the GitHub official actions like:\n", + "\n", + "- `actions/setup-node@v3` - Sets up Node.js environment\n", + "- `actions/cache@v3` - Caches dependencies and build outputs\n", + "- `actions/upload-artifact@v3` - Uploads workflow artifacts\n", + "- `actions/download-artifact@v3` - Downloads workflow artifacts\n", + "- `actions/labeler@v4` - Automatically labels pull requests\n", + "- `actions/stale@v8` - Marks and closes stale issues/PRs\n", + "- `actions/dependency-review-action@v3` - Reviews dependency changes\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Events and triggers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Events are specific activities that trigger a workflow. Common triggers include:\n", + "\n", + "- `push`: When code is pushed to the repository\n", + "- `pull_request`: When a PR is opened or updated\n", + "- `schedule`: At specified times using cron syntax\n", + "- `workflow_dispatch`: Manual trigger via GitHub UI\n", + "\n", + "Here is how you can configure multiple triggers:\n", + "\n", + "```yaml\n", + "name: Comprehensive Workflow\n", + "on:\n", + " push:\n", + " branches: [main]\n", + " pull_request:\n", + " branches: [main]\n", + " schedule:\n", + " - cron: '0 0 * * *' # Daily at midnight\n", + " workflow_dispatch: # Manual trigger\n", + "\n", + "jobs:\n", + " process:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " - name: Run daily tasks\n", + " run: python daily_tasks.py\n", + " env:\n", + " API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example shows how a single workflow can:\n", + "\n", + "- Run automatically on code changes on `git push`\n", + "- Execute daily scheduled tasks with cron\n", + "- Be triggered automatically when needed through the GitHub UI\n", + "- Handle sensitive data like API keys securely" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cron jobs in GitHub Actions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To use the `schedule` trigger effectively in GitHub Actions, you'll need to understand cron syntax. This powerful scheduling format lets you automate workflows to run at precise times. The syntax uses five fields to specify when a job should run:\n", + "\n", + "![](images/cron-syntax.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are some common cron schedule examples:\n", + "\n", + "```yaml\n", + "# Daily at 3:30 AM UTC\n", + "- cron: '30 3 * * *'\n", + "\n", + "# Every Monday at 1:00 PM UTC\n", + "- cron: '0 13 * * 1'\n", + "\n", + "# Every 6 hours at the first minute\n", + "- cron: '0 */6 * * *'\n", + "\n", + "# At minute 15 of every hour\n", + "- cron: '15 * * * *'\n", + "\n", + "# Every weekday (Monday through Friday)\n", + "- cron: '0 0 * * 1-5'\n", + "\n", + "# Each day at 12am, 6am, 12pm, 6pm on Tuesday, Thursday, Saturday\n", + "- cron: '0 0,6,12,18 * * 1,3,5'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is a sample workflow for a scraping job with four different schedules (multiple schedules are allowed):\n", + "\n", + "```yaml\n", + "name: Price Scraper Schedules\n", + "on:\n", + " schedule:\n", + " - cron: '0 */4 * * *' # Every 4 hours\n", + " - cron: '30 1 * * *' # Daily at 1:30 AM UTC\n", + " - cron: '0 9 * * 1-5' # Weekdays at 9 AM UTC\n", + "\n", + "jobs:\n", + " scrape:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " - name: Run Firecrawl scraper\n", + " env:\n", + " API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}\n", + " run: python scraper.py\n", + "```\n", + "\n", + "Remember that GitHub Actions runs on UTC time, and schedules might experience slight delays during peak GitHub usage. That's why it's helpful to combine `schedule` with `workflow_dispatch` as we saw earlier - giving you both automated and manual trigger options." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "---------------\n", + "\n", + "Understanding these core concepts allows you to create workflows that are efficient (running only when needed), secure (properly handling sensitive data), maintainable (using reusable actions) and scalable (running on different platforms). \n", + "\n", + "In the next section, we will put these concepts into practice by creating your first GitHub actions workflow." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating Your First GitHub Actions Workflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create a practical GitHub Actions workflow from scratch. We'll build a workflow that automatically tests a Python script and runts it on a schedule - a universal task applicable to any programming language. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setting up the environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's start by creating a working directory for this mini-project:\n", + "\n", + "```bash\n", + "mkdir first-workflows\n", + "cd first-workflows\n", + "```\n", + "\n", + "Let's create the standard `.github/workflows` folder structure GitHub uses for detecting workflow files:\n", + "\n", + "```bash\n", + "mkdir -p .github/workflows\n", + "```\n", + "\n", + "The workflow files can have any name but must have a `.yml` extension:\n", + "\n", + "```bash\n", + "touch .github/workflows/system_monitor.yml\n", + "```\n", + "\n", + "In addition to the workflows folder, create a `tests` folder as well as a test file:\n", + "\n", + "```bash\n", + "mkdir tests\n", + "touch tests/test_main.py\n", + "```\n", + "\n", + "We should also create the `main.py` file along with a `requirements.txt`:\n", + "\n", + "```bash\n", + "touch main.py requirements.txt\n", + "```\n", + "\n", + "Then, add these two dependencies to `requirements.txt`:\n", + "\n", + "```text\n", + "psutil>=5.9.0\n", + "pytest>=7.0.0\n", + "```\n", + "\n", + "Finally, let's initialize git and make our first commit:\n", + "\n", + "```bash\n", + "git init \n", + "git add .\n", + "git commit -m \"Initial commit\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check out the [Git documentation](https://git-scm.com/doc) if you don't have it installed already." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Writing your first workflow file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's write the workflow logic first. Open `system_monitor.yml` and paste each code snippet we are about to define one after the other. \n", + "\n", + "1. Workflow name and triggers:\n", + "\n", + "```yaml\n", + "name: System Monitoring\n", + "on:\n", + " schedule:\n", + " - cron: '*/30 * * * *' # Run every 30 minutes\n", + " workflow_dispatch: # Enables manual trigger\n", + "```\n", + "\n", + "In this part, we give a descriptive name to the workflow that appears in GitHub's UI. Using the `on` field, we set the workflow to run every 30 minutes and through a manual trigger." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "2. Job definition:\n", + "\n", + "```yaml\n", + "jobs:\n", + " run_script:\n", + " runs-on: ubuntu-latest\n", + "```\n", + "\n", + "`jobs` contains all the jobs in this workflow and it has a `run_script` name, which is a unique identifier. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3. Steps:\n", + "\n", + "There are five steps that run sequentially in this workflow. They are given descriptive names that appear in the GitHub UI and uses official GitHub actions and custom terminal commands. \n", + "\n", + "```yaml\n", + "jobs:\n", + " monitor:\n", + " runs-on: ubuntu-latest\n", + " \n", + " steps:\n", + " - name: Check out repository\n", + " uses: actions/checkout@v3\n", + " \n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: '3.9'\n", + " \n", + " - name: Install dependencies\n", + " run: |\n", + " python -m pip install --upgrade pip\n", + " pip install -r requirements.txt\n", + " \n", + " - name: Run tests\n", + " run: pytest tests/\n", + " \n", + " - name: Collect system metrics\n", + " run: python main.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is what each step does:\n", + "\n", + "1. Check out repository code with `actions/checkout@v3`.\n", + "2. Configures Python 3.9 environment.\n", + "3. Runs two terminal commands that:\n", + " - Install/upgrade `pip`\n", + " - Install `pytest` package\n", + "4. Runs the tests located in the `tests` directory using `pytest`.\n", + "5. Executes the main script with `python main.py`. \n", + "\n", + "Notice the use of `|` (pipe) operator for multi-line commands.\n", + "\n", + "After you complete writing the workflow, commit the changes to Git:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a workflow file for monitoring system resources\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating the Python script" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's write the `main.py` file, which is a monitoring script that helps software developers track system resource usage over time, enabling them to identify performance bottlenecks and capacity issues in their development environment. \n", + "\n", + "```python\n", + "import psutil\n", + "import json\n", + "from datetime import datetime\n", + "from pathlib import Path\n", + "```\n", + "\n", + "This script collects and logs system metrics over time. It uses `psutil` to gather CPU usage, memory usage, disk usage, and active process counts. The metrics are timestamped and saved to JSON files organized by date.\n", + "\n", + "The script has three main functions:\n", + "\n", + "```python\n", + "def get_system_metrics():\n", + " \"\"\"Collect key system metrics\"\"\"\n", + " metrics = {\n", + " \"cpu_percent\": psutil.cpu_percent(interval=1),\n", + " \"memory_percent\": psutil.virtual_memory().percent,\n", + " \"disk_usage\": psutil.disk_usage('/').percent,\n", + " \"timestamp\": datetime.now().isoformat()\n", + " }\n", + " \n", + " # Add running processes count\n", + " metrics[\"active_processes\"] = len(psutil.pids())\n", + " \n", + " return metrics\n", + "```\n", + "\n", + "`get_system_metrics()` - Collects current system metrics including CPU percentage, memory usage percentage, disk usage percentage, timestamp, and count of active processes.\n", + "\n", + "```python\n", + "def save_metrics(metrics):\n", + " \"\"\"Save metrics to a JSON file with today's date\"\"\"\n", + " date_str = datetime.now().strftime(\"%Y-%m-%d\")\n", + " reports_dir = Path(\"system_metrics\")\n", + " reports_dir.mkdir(exist_ok=True)\n", + " \n", + " # Save to daily file\n", + " file_path = reports_dir / f\"metrics_{date_str}.json\"\n", + " \n", + " # Load existing metrics if file exists\n", + " if file_path.exists():\n", + " with open(file_path) as f:\n", + " daily_metrics = json.load(f)\n", + " else:\n", + " daily_metrics = []\n", + " \n", + " # Append new metrics\n", + " daily_metrics.append(metrics)\n", + " \n", + " # Save updated metrics\n", + " with open(file_path, 'w') as f:\n", + " json.dump(daily_metrics, f, indent=2)\n", + "```\n", + "\n", + "`save_metrics()` - Handles saving the metrics to JSON files. It creates a `system_metrics` directory if needed, and saves metrics to date-specific files (e.g. `metrics_2024-12-12.json`). If a file for the current date exists, it loads and appends to it, otherwise creates a new file.\n", + "\n", + "```python\n", + "def main():\n", + " try:\n", + " metrics = get_system_metrics()\n", + " save_metrics(metrics)\n", + " print(f\"System metrics collected at {metrics['timestamp']}\")\n", + " print(f\"CPU: {metrics['cpu_percent']}% | Memory: {metrics['memory_percent']}%\")\n", + " return True\n", + " except Exception as e:\n", + " print(f\"Error collecting metrics: {str(e)}\")\n", + " return False\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "```\n", + "\n", + "`main()` - Orchestrates the metric collection and saving process. It calls `get_system_metrics()`, saves the data via `save_metrics()`, prints current CPU and memory usage to console, and handles any errors that occur during execution.\n", + "\n", + "The script can be run directly or imported as a module. When run directly (which is what happens in a GitHub Actions workflow), it executes the `main()` function which collects and saves one set of metrics." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Combine the code snippets above into the `main.py` file and commit the changes:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add the main.py functionality\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding tests" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Testing is a critical part of software engineering workflows for several reasons:\n", + "\n", + "1. Reliability: Tests help ensure code behaves correctly and consistently across changes.\n", + "2. Regression prevention: Tests catch when new changes break existing functionality.\n", + "3. Documentation: Tests serve as executable documentation of expected behavior\n", + "4. Design feedback: Writing tests helps identify design issues early\n", + "5. Confidence: A good test suite gives confidence when refactoring or adding features\n", + "\n", + "For our system metrics collection script, tests would be valuable to verify:\n", + "\n", + "- The `get_system_metrics()` function returns data in the expected format with valid ranges\n", + "- The `save_metrics()` function properly handles file operations and JSON serialization\n", + "- Error handling works correctly for various failure scenarios\n", + "- The `main()` function orchestrates the workflow as intended\n", + "\n", + "With that said, let's work on the `tests/test_main.py` file:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import json\n", + "from datetime import datetime\n", + "from pathlib import Path\n", + "from main import get_system_metrics, save_metrics\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The test file we are about to write demonstrates key principles of testing with `pytest`, a popular Python testing framework. Pytest makes it easy to write tests by using simple `assert` statements and providing a rich set of features for test organization and execution. The test functions are automatically discovered by `pytest` when their names start with `test_`, and each function tests a specific aspect of the system's functionality.\n", + "\n", + "```python\n", + "def test_get_system_metrics():\n", + " \"\"\"Test if system metrics are collected correctly\"\"\"\n", + " metrics = get_system_metrics()\n", + " \n", + " # Check if all required metrics exist and are valid\n", + " assert 0 <= metrics['cpu_percent'] <= 100\n", + " assert 0 <= metrics['memory_percent'] <= 100\n", + " assert metrics['active_processes'] > 0\n", + " assert 'timestamp' in metrics\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we have two test functions that verify different components of our metrics collection system. The first test, `test_get_system_metrics()`, checks if the metrics collection function returns data in the expected format and with valid ranges. It uses multiple assert statements to verify that CPU and memory percentages are between 0 and 100, that there are active processes, and that a timestamp is included. This demonstrates the practice of testing both the structure of returned data and the validity of its values.\n", + "\n", + "```python\n", + "def test_save_and_read_metrics():\n", + " \"\"\"Test if metrics are saved and can be read back\"\"\"\n", + " # Get and save metrics\n", + " metrics = get_system_metrics()\n", + " save_metrics(metrics)\n", + " \n", + " # Check if file exists and contains data\n", + " date_str = datetime.now().strftime(\"%Y-%m-%d\")\n", + " file_path = Path(\"system_metrics\") / f\"metrics_{date_str}.json\"\n", + " \n", + " assert file_path.exists()\n", + " with open(file_path) as f:\n", + " saved_data = json.load(f)\n", + " \n", + " assert isinstance(saved_data, list)\n", + " assert len(saved_data) > 0\n", + "```\n", + "\n", + "The second test, `test_save_and_read_metrics()`, showcases integration testing by verifying that metrics can be both saved to and read from a file. It follows a common testing pattern: arrange (setup the test conditions), act (perform the operations being tested), and assert (verify the results). The test ensures that the file is created in the expected location and that the saved data maintains the correct structure. This type of test is particularly valuable as it verifies that different components of the system work together correctly.\n", + "\n", + "Combine the above code snippets and commit the changes to GitHub:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "git add .\n", + "git commit -m \"Write tests for main.py\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Running your first GitHub Actions workflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we've created our system monitoring workflow, let's set it up on GitHub and run it. First, push everything we have to a new GitHub repository:\n", + "\n", + "```bash\n", + "git remote add origin https://github.com/your-username/your-repository.git\n", + "git branch -M main\n", + "git push -u origin main\n", + "```\n", + "\n", + "Once the workflow file is pushed, GitHub automatically detects it and displays in the \"Actions\" tab of your repository. The workflow is scheduled so you don't need to do anything - the first workflow run will happen within 30 minutes (remember how we set the running interval to `*/30` with cron). Since the workflow also includes a `workflow_dispatch` field, you can trigger it manually by clicking on the \"Run workflow\" button. \n", + "\n", + "After clicking the button, a new run appears within a few seconds (refresh if you don't see it). To see the workflow run in real-time, click on it and expand the `monitor` job. You'll see each step executing:\n", + "\n", + "- Checking out repository\n", + "- Setting up Python\n", + "- Installing dependencies\n", + "- Running tests\n", + "- Collecting metrics" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Committing changes made by GitHub Actions workflows" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Right now, our workflow file has a problem - while it successfully collects metrics, it doesn't commit and push the changes back to the repository. This means that although metrics are being gathered, they aren't being saved in version control. Let's modify the workflow to automatically commit and push the collected metrics:\n", + "\n", + "```yaml\n", + "name: System Monitor\n", + "\n", + "on:\n", + " schedule:\n", + " - cron: '*/30 * * * *'\n", + " workflow_dispatch:\n", + "\n", + "permissions:\n", + " contents: write\n", + "\n", + "jobs:\n", + " monitor:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " \n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: '3.10'\n", + " \n", + " - name: Install dependencies\n", + " run: |\n", + " python -m pip install --upgrade pip\n", + " pip install -r requirements.txt\n", + " \n", + " - name: Run tests\n", + " run: python -m pytest\n", + " \n", + " - name: Collect metrics\n", + " run: python main.py\n", + " \n", + " - name: Commit and push changes\n", + " run: |\n", + " git config --global user.name 'github-actions[bot]'\n", + " git config --global user.email 'github-actions[bot]@users.noreply.github.com'\n", + " git add metrics.json\n", + " git commit -m \"Update metrics\" || exit 0\n", + " git push\n", + "```\n", + "\n", + "The key changes in this updated workflow are:\n", + "\n", + "1. Added `permissions` block with `contents: write` to allow the workflow to push changes back to the repository.\n", + "2. Added a new \"Commit and push changes\" step that:\n", + " - Configures git user identity as `github-actions` bot\n", + " - Stages the `metrics.json` file\n", + " - Creates a commit with message \"Update metrics\" \n", + " - Pushes the changes back to the repository\n", + " \n", + "The \"|| exit 0\" after `git commit` ensures the workflow doesn't fail if there are no changes to commit.\n", + "\n", + "This allows the workflow to automatically save and version control the metrics it collects. Let's commit the changes to the workflow and push:\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a commit step to the workflow file\"\n", + "git push origin main\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After this change, try running the workflow manually and verify its success by navigating to the Actions tab in your GitHub repository. You should see the workflow run and the `metrics.json` file updated with new data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Managing Sensitive Data and Environment Variables" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When building automated workflows with GitHub Actions, proper handling of sensitive data like API keys, passwords, and access tokens is crucial for security. Let's explore best practices for managing these credentials." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Understanding environment variables in GitHub Actions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Environment variables in GitHub Actions can be set at different levels:\n", + "\n", + "- Repository level (GitHub Secrets)\n", + "- Workflow level\n", + "- Job level\n", + "- Step level\n", + "\n", + "Here is how to properly configure and use them:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 1. Setting up repository secrets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, store sensitive values as repository secrets:\n", + "\n", + "1. Navigate to your GitHub repository\n", + "2. Go to Settings → Secrets and variables → Actions\n", + "3. Click \"New repository secret\"\n", + "4. Add your secrets with descriptive names like:\n", + "\n", + "- `API_KEY`\n", + "- `DATABASE_URL`\n", + "- `AWS_ACCESS_KEY`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 2. Using secrets in workflows" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Reference secrets in your workflow file using the `secrets` context:\n", + "\n", + "```yaml\n", + "name: Web Scraping Pipeline\n", + "# ... the rest of the file\n", + "\n", + "jobs:\n", + " scrape:\n", + " runs-on: ubuntu-latest\n", + " \n", + " steps:\n", + " # ... the rest of the steps\n", + " \n", + " - name: Run scraper\n", + " env:\n", + " API_KEY: ${{ secrets.API_KEY }}\n", + " DATABASE_URL: ${{ secrets.DATABASE_URL }}\n", + " run: python scraper.py\n", + "```\n", + "\n", + "Above, the \"Run scraper\" step executes `scraper.py` which relies on two environment variables configured through the `secrets` context. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 3. Local development with .env files" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For local development, use `.env` files to manage environment variables:\n", + "\n", + "```bash\n", + "touch .env\n", + "echo \"API_KEY='your-api-key-here'\" >> .env\n", + "echo \"DATABASE_URL='postgresql://user:pass@localhost:5432/db'\" >> .env\n", + "```\n", + "\n", + "Create a `.gitignore` file to prevent committing sensitive data:\n", + "\n", + "```bash\n", + "echo \".env\" >> .gitignore\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 4. Loading environment variables in Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use `python-dotenv` to load variables from `.env` files:\n", + "\n", + "```python\n", + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "# Load environment variables from .env file\n", + "load_dotenv()\n", + "\n", + "# Access variables\n", + "api_key = os.getenv('API_KEY')\n", + "database_url = os.getenv('DATABASE_URL')\n", + "\n", + "if not api_key or not database_url:\n", + " raise ValueError(\"Missing required environment variables\")\n", + "```\n", + "\n", + "This code demonstrates loading environment variables from a `.env` file using `python-dotenv`. The `load_dotenv()` function reads the variables, which can then be accessed via `os.getenv()`. Basic validation ensures required variables exist." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 5. Environment variable validation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a configuration class to validate environment variables:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "from pydantic import BaseSettings, SecretStr\n", + "\n", + "\n", + "class Settings(BaseSettings):\n", + " api_key: SecretStr\n", + " database_url: str\n", + " debug_mode: bool = False\n", + "\n", + " class Config:\n", + " env_file = \".env\"\n", + " env_file_encoding = \"utf-8\"\n", + "\n", + "\n", + "# Initialize settings\n", + "settings = Settings()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This approach using Pydantic provides several advantages for environment variable management:\n", + "\n", + "1. Type validation - Pydantic automatically validates types and converts values\n", + "2. Default values - The `debug_mode` demonstrates setting defaults\n", + "3. Secret handling - `SecretStr` provides secure handling of sensitive values\n", + "4. Centralized config - All environment variables are defined in one place\n", + "5. IDE support - Get autocomplete and type hints when using the settings object\n", + "\n", + "The `Settings` class inherits from `BaseSettings` which automatically loads from environment variables. The `Config` class specifies to also load from a `.env` file.\n", + "\n", + "Using `settings = Settings()` creates a validated configuration object that can be imported and used throughout the application. This is more robust than accessing `os.environ` directly.\n", + "\n", + "Example usage:\n", + "\n", + "```python\n", + "settings.api_key.get_secret_value() # Securely access API key\n", + "settings.database_url # Type-checked database URL\n", + "settings.debug_mode # Boolean with default value\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 6. Handle different environments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Handle different environments (development, staging, production) using environment-specific files:\n", + "\n", + "```bash\n", + ".env # Default environment variables\n", + ".env.development # Development-specific variables\n", + ".env.staging # Staging-specific variables\n", + ".env.production # Production-specific variables\n", + "```\n", + "\n", + "Load the appropriate file based on the environment:\n", + "\n", + "```bash\n", + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "env = os.getenv('ENVIRONMENT', 'development')\n", + "env_file = f'.env.{env}'\n", + "\n", + "load_dotenv(env_file)\n", + "```\n", + "\n", + "This approach allows you to maintain separate configurations for different environments while keeping sensitive information secure. The environment-specific files can contain different values for the same variables, such as:\n", + "\n", + "- Development environment may use local services and dummy credentials\n", + "- Staging environment may use test services with restricted access\n", + "- Production environment contains real credentials and production service endpoints\n", + "\n", + "You can also combine this with the `Pydantic` settings approach shown earlier for robust configuration management across environments.\n", + "\n", + "For example, staging might use a test database while production uses the live database:\n", + "\n", + "```bash\n", + "# .env.staging:\n", + "DATABASE_URL=postgresql://test-db.example.com\n", + "API_KEY=test-key\n", + "```\n", + "\n", + "```bash\n", + ".env.production:\n", + "DATABASE_URL=postgresql://prod-db.example.com \n", + "API_KEY=live-key\n", + "```\n", + "\n", + "This separation helps prevent accidental use of production resources during development and testing." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building Real-World Python Workflows" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's explore three practical examples of GitHub Actions workflows for common Python tasks: web scraping, package publishing, and container builds." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Scheduled web scraping with Firecrawl" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Web scraping is a common use case for automated workflows. Let's build a workflow that scrapes [Hacker News](https://news.ycombinator.com/) on a schedule using [Firecrawl](https://docs.firecrawl.dev), which is a Python AI-based web scraping engine designed for large-scale data collection. Here are some key benefits that make Firecrawl an excellent choice for this task:\n", + "\n", + "1. **Enterprise-grade automation and scalability** - Firecrawl streamlines web scraping with powerful automation features.\n", + "2. **AI-powered content extraction** - Maintains scraper reliability over time by identifying and extracting data based on semantic descriptions instead of relying HTML elements and CSS selectors.\n", + "3. **Handles complex scraping challenges** - Automatically manages proxies, anti-bot mechanisms, and dynamic JavaScript content.\n", + "4. **Multiple output formats** - Supports scraping and converting data in markdown, tabular, screenshots, and HTML, making it versatile for various applications.\n", + "5. **Built-in rate limiting and request management** - Ensures efficient and compliant data extraction.\n", + "6. **Geographic location customization** - Avoids IP bans by customizing the geographic location of requests." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's build our web scraping workflow using Firecrawl to demonstrate these capabilities.\n", + "\n", + "```bash\n", + "# Create project directory and install dependencies\n", + "mkdir hacker-news-scraper && cd hacker-news-scraper\n", + "pip install firecrawl-py pydantic python-dotenv\n", + "\n", + "# Create necessary files\n", + "touch requirements.txt scraper.py .env\n", + "\n", + "# Add dependencies to requirements.txt\n", + "echo \"firecrawl-py\\npydantic\\npython-dotenv\" > requirements.txt\n", + "\n", + "# Add Firecrawl API key to .env (get your key at firecrawl.dev/signin/signup)\n", + "echo \"FIRECRAWL_API_KEY='your_api_key_here'\" > .env\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Open the scraper script where we define our scraping logic:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# scraper.py\n", + "import json\n", + "from firecrawl import FirecrawlApp\n", + "from dotenv import load_dotenv\n", + "from pydantic import BaseModel, Field\n", + "from typing import List\n", + "from datetime import datetime\n", + "\n", + "load_dotenv()\n", + "\n", + "BASE_URL = \"https://news.ycombinator.com/\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we import necessary libraries and packages, also defining a base URL we are going to scrape." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class NewsItem(BaseModel):\n", + " title: str = Field(description=\"The title of the news item\")\n", + " source_url: str = Field(description=\"The URL of the news item\")\n", + " author: str = Field(\n", + " description=\"The URL of the post author's profile concatenated with the base URL.\"\n", + " )\n", + " rank: str = Field(description=\"The rank of the news item\")\n", + " upvotes: str = Field(description=\"The number of upvotes of the news item\")\n", + " date: str = Field(description=\"The date of the news item.\")\n", + "\n", + "\n", + "class NewsData(BaseModel):\n", + " news_items: List[NewsItem]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define two Pydantic models to structure our scraped data:\n", + "\n", + "1. `NewsItem` - Represents a single news item with fields for title, URL, author, rank, upvotes and date\n", + "2. `NewsData` - Contains a list of `NewsItem` objects\n", + "\n", + "These models help validate the scraped data and ensure it matches our expected schema. They also make it easier to serialize/deserialize the data when saving to JSON. Using `Field` with a detailed description is crucial because Firecrawl uses these definitions to automatically detect the HTMl elements and CSS selectors we are looking for.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def get_news_data():\n", + " app = FirecrawlApp()\n", + "\n", + " data = app.scrape_url(\n", + " BASE_URL,\n", + " params={\n", + " \"formats\": [\"extract\"],\n", + " \"extract\": {\"schema\": NewsData.model_json_schema()},\n", + " },\n", + " )\n", + "\n", + " return data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `get_news_data()` function uses Firecrawl to scrape Hacker News. It creates a `FirecrawlApp` instance and calls `scrape_url()` with the `BASE_URL` and parameters specifying we want to extract data according to our `NewsData` schema. The schema helps Firecrawl automatically identify and extract the relevant HTML elements. The function returns the scraped data containing news items with their titles, URLs, authors, ranks, upvotes and dates." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def save_firecrawl_news_data():\n", + " \"\"\"\n", + " Save the scraped news data to a JSON file with the current date in the filename.\n", + " \"\"\"\n", + " # Get the data\n", + " data = get_news_data()\n", + " # Format current date for filename\n", + " date_str = datetime.now().strftime(\"%Y_%m_%d_%H_%M\")\n", + " filename = f\"firecrawl_hacker_news_data_{date_str}.json\"\n", + "\n", + " # Save the news items to JSON file\n", + " with open(filename, \"w\") as f:\n", + " json.dump(data[\"extract\"][\"news_items\"], f, indent=4)\n", + "\n", + " print(f\"{datetime.now()}: Successfully saved the news data.\")\n", + " \n", + "if __name__ == \"__main__\":\n", + " save_firecrawl_news_data()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `save_firecrawl_news_data()` function handles saving the scraped Hacker News data to a JSON file. It first calls `get_news_data()` to fetch the latest data from Hacker News. Then it generates a filename using the current timestamp to ensure uniqueness. The data is saved to a JSON file with that filename, with the news items formatted with proper indentation for readability. Finally, it prints a confirmation message with the current timestamp when the save is complete. This function provides a convenient way to store snapshots of Hacker News data that can be analyzed later.\n", + "\n", + "Combine these snippets into the `scraper.py` script. Then, we can write a workflow that executes it on schedule:\n", + "\n", + "```bash\n", + "cd .. # Change back to the project root directory\n", + "touch .github/workflows/hacker-news-scraper.py # Create the workflow file\n", + "```\n", + "\n", + "Here is what the workflow file must look like:\n", + "\n", + "```yaml\n", + "name: Run Hacker News Scraper\n", + "\n", + "permissions:\n", + " contents: write\n", + "\n", + "on:\n", + " schedule:\n", + " - cron: \"0 */6 * * *\"\n", + " workflow_dispatch:\n", + "\n", + "jobs:\n", + " scrape:\n", + " runs-on: ubuntu-latest\n", + " \n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " \n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: \"3.10\"\n", + " \n", + " - name: Install dependencies\n", + " run: |\n", + " python -m pip install --upgrade pip\n", + " pip install -r hacker-news-scraper/requirements.txt\n", + " \n", + " - name: Run scraper\n", + " run: python hacker-news-scraper/scraper.py\n", + " \n", + " - name: Commit and push if changes\n", + " run: |\n", + " git config --local user.email \"github-actions[bot]@users.noreply.github.com\"\n", + " git config --local user.name \"github-actions[bot]\"\n", + " git add .\n", + " git commit -m \"Update scraped data\" -a || exit 0\n", + " git push\n", + "```\n", + "\n", + "This workflow runs our Hacker News scraper every 6 hours using GitHub Actions. It sets up Python, installs dependencies, executes the scraper, and automatically commits any new data to the repository. The workflow can also be triggered manually using the `workflow_dispatch` event. One important note about the paths specified in the workflow file is that they must match your repository's directory structure exactly, including the requirements.txt location and the path to your scraper script.\n", + "\n", + "To enable the workflow, simply push all the changes to GitHub and test it through the UI. The next runs will be automatic.\n", + "\n", + "```bash\n", + "git add .\n", + "git commit -m \"Add a scraping workflow\"\n", + "git push origin main\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Package publishing workflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Publishing Python packages to PyPI (Python Package Index) typically involves several steps. First, developers need to prepare their package by creating a proper directory structure, writing setup files, and ensuring all metadata is correct. Then, the package needs to be built into distribution formats - both source distributions (`sdist`) and wheel distributions (`bdist_wheel`). Finally, these distribution files are uploaded to PyPI using tools like `twine`. This process often requires careful version management and proper credentials for the package repository. While this can be done manually, automating it with CI/CD pipelines like GitHub Actions ensures consistency and reduces human error in the release process.\n", + "\n", + "For example, the following workflow publishes a new version of a package when you create a new release:\n", + "\n", + "```yaml\n", + "name: Publish Python Package\n", + "on:\n", + " release:\n", + " types: [created]\n", + "\n", + "jobs:\n", + " deploy:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " \n", + " - name: Set up Python\n", + " uses: actions/setup-python@v4\n", + " with:\n", + " python-version: '3.10'\n", + " \n", + " - name: Install dependencies\n", + " run: |\n", + " python -m pip install --upgrade pip\n", + " pip install build twine\n", + " \n", + " - name: Build package\n", + " run: python -m build\n", + " \n", + " - name: Publish to PyPI\n", + " env:\n", + " TWINE_USERNAME: __token__\n", + " TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}\n", + " run: |\n", + " python -m twine upload dist/*\n", + "```\n", + "\n", + "The workflow automates publishing Python packages to PyPI when GitHub releases are created. \n", + "\n", + "Required setup steps:\n", + "1. Package must have `setup.py` or `pyproject.toml` configured\n", + "2. Create PyPI account at `pypi.org`\n", + "3. Generate PyPI API token with upload permissions\n", + "4. Store token as `PYPI_API_TOKEN` in repository secrets\n", + "\n", + "The workflow process:\n", + "1. Triggers on new GitHub release\n", + "2. Checks out code and sets up Python\n", + "3. Installs build tools\n", + "4. Creates distribution packages\n", + "5. Uploads to PyPI using stored token\n", + "\n", + "The `__token__` username is used with PyPI's token authentication, while the actual token is accessed securely through GitHub secrets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Container build and push workflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GitHub Actions can also automate building and pushing Docker containers to container registries like Docker Hub or GitHub Container Registry (GHCR). This workflow is useful for maintaining containerized applications and ensuring your latest code changes are packaged into updated container images.\n", + "\n", + "The process typically involves:\n", + "\n", + "1. Building a Docker image from your `Dockerfile`\n", + "2. Tagging the image with version/metadata\n", + "3. Authenticating with the container registry\n", + "4. Pushing the tagged image to the registry\n", + "\n", + "This automation ensures your container images stay in sync with code changes and are readily available for deployment. Here is a sample workflow containing these steps:\n", + "\n", + "```yaml\n", + "name: Build and Push Container\n", + "on:\n", + " push:\n", + " branches: [main]\n", + " paths:\n", + " - 'Dockerfile'\n", + " - 'src/**'\n", + " workflow_dispatch:\n", + "\n", + "jobs:\n", + " build:\n", + " runs-on: ubuntu-latest\n", + " steps:\n", + " - uses: actions/checkout@v3\n", + " \n", + " - name: Set up Docker Buildx\n", + " uses: docker/setup-buildx-action@v2\n", + " \n", + " - name: Login to Docker Hub\n", + " uses: docker/login-action@v2\n", + " with:\n", + " username: ${{ secrets.DOCKERHUB_USERNAME }}\n", + " password: ${{ secrets.DOCKERHUB_TOKEN }}\n", + " \n", + " - name: Build and push\n", + " uses: docker/build-push-action@v4\n", + " with:\n", + " context: .\n", + " push: true\n", + " tags: |\n", + " user/app:latest\n", + " user/app:${{ github.sha }}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This workflow introduces a few new GitHub Actions concepts and syntax:\n", + "\n", + "The `paths` trigger filter ensures the workflow only runs when changes are made to the Dockerfile or files in the `src` directory, preventing unnecessary builds.\n", + "\n", + "`docker/setup-buildx-action` configures Docker Buildx, which provides enhanced build capabilities including multi-platform builds and build caching.\n", + "\n", + "`docker/login-action` handles registry authentication. Before using this, you must:\n", + "\n", + "1. [Create a Docker Hub account](https://app.docker.com/signup)\n", + "2. Generate an access token in Docker Hub settings\n", + "3. Add `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` as repository secrets in GitHub\n", + "\n", + "`docker/build-push-action` is a specialized action for building and pushing Docker images. The configuration shows:\n", + "- `context: .` (builds from current directory)\n", + "- `push: true` (automatically pushes after building)\n", + "- `tags:` specifies multiple tags including:\n", + " - `latest:` rolling tag for most recent version\n", + " - `github.sha:` unique tag using commit hash for versioning\n", + "\n", + "The workflow assumes you have:\n", + "- A valid Dockerfile in your repository\n", + "- Required application code in `src` directory\n", + "- Docker Hub repository permissions\n", + "- Properly configured repository secrets\n", + "\n", + "When this workflow runs successfully, it produces a containerized version of your application that is automatically published to Docker Hub and can be pulled with either the latest tag or specific commit hash." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Throughout this tutorial, we've explored the fundamentals and practical applications of GitHub Actions for Python development. From understanding core concepts like workflows, jobs, and actions, to implementing real-world examples including automated testing, web scraping, package publishing, and container builds, you've gained hands-on experience with this powerful automation platform. We've also covered critical aspects like managing sensitive data through environment variables and secrets, ensuring your automated workflows are both secure and maintainable.\n", + "\n", + "As you continue your journey with GitHub Actions, remember that automation is an iterative process. Start small with basic workflows, test thoroughly, and gradually add complexity as needed. The examples provided here serve as templates that you can adapt and expand for your specific use cases. For further learning, explore the [GitHub Actions documentation](https://docs.github.com/en/actions), join the [GitHub Community Forum](https://github.community/), and experiment with the vast ecosystem of pre-built actions available in the [GitHub Marketplace](https://github.com/marketplace?type=actions). Whether you're building a personal project or managing enterprise applications, GitHub Actions provides the tools you need to streamline your development workflow and focus on what matters most - writing great code.\n", + "\n", + "If you want to learn more about Firecrawl, the web scraping API we used today, you can read the following posts:\n", + "\n", + "- [Guide to Scheduling Web Scrapers in Python](https://www.firecrawl.dev/blog/automated-web-scraping-free-2025)\n", + "- [Mastering Firecrawl's Scrape Endpoint](https://www.firecrawl.dev/blog/mastering-firecrawl-scrape-endpoint)\n", + "- [Getting Started With Predicted Outputs in OpenAI](https://www.firecrawl.dev/blog/getting-started-with-predicted-outputs-openai)\n", + "\n", + "Thank you for reading!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/blog-articles/github-actions-tutorial/notebook.md b/examples/blog-articles/github-actions-tutorial/notebook.md new file mode 100644 index 00000000..332a1c57 --- /dev/null +++ b/examples/blog-articles/github-actions-tutorial/notebook.md @@ -0,0 +1,1187 @@ +--- +title: Comprehensive GitHub Actions Tutorial For Beginners With Examples in Python +description: Learn how to automate software development workflows with GitHub Actions. This beginner-friendly tutorial covers workflow creation, CI/CD pipelines, scheduled tasks, and practical Python examples to help you streamline your development process. +slug: github-actions-tutorial-for-beginners-with-python-examples +date: Dec 9, 2024 +author: bex_tuychiev +image: /images/blog/github-actions-tutorial/github-actions-tutorial-for-beginners-with-python-examples.jpg +categories: [tutorials] +keywords: [github actions, github actions tutorial, github actions environment variables, github actions secrets, github actions workflow, github actions run, github actions jobs] +--- + +## Introduction + +GitHub Actions is a powerful automation platform that helps developers automate repetitive, time-consuming software development workflows. Instead of manually running tests, executing scripts at intervals, or performing any programmable task, you can let GitHub Actions handle those operations when specific events occur in your repository. In this tutorial, you will learn how to use this critical feature of GitHub and design your own workflows for several real-world use cases. + +### What are GitHub Actions? + +At its core, [GitHub Actions](https://docs.github.com/en/actions) is a continuous integration and continuous delivery (CI/CD) platform that lets you automate various tasks directly from your GitHub repository. Think of it as your personal automation assistant, which can: + +- Run your Python tests automatically when you push code +- Deploy your application when you create a new release +- Send notifications when issues are created +- Schedule tasks to run at specific times +- And much more... + +### Why automate with GitHub Actions? + +Consider this common scenario: You are building a Python application that scrapes product prices from various e-commerce websites. Without GitHub Actions, you would need to: + +1. Manually run your tests after each code change +2. Remember to execute the scraper at regular intervals +3. Deploy updates to your production environment +4. Keep track of environment variables and secrets + +With GitHub Actions, all of these tasks can be automated through workflows, typically written in YAML files like this: + +```yaml +name: Run Price Scraper + +on: + schedule: + - cron: '0 */12 * * *' # Runs every 12 hours + workflow_dispatch: # Allows manual triggers + +jobs: + scrape: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Run scraper + env: + API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} + run: python scraper.py +``` + +This workflow automatically runs a scraper every 12 hours, handles Python version setup, and securely manages API keys—all without manual intervention. + +### What we'll build in this tutorial + +Throughout this tutorial, we'll build several practical GitHub Actions workflows for Python applications. You will learn how to: + +1. Create basic and advanced workflow configurations +2. Work with environment variables and secrets +3. Set up automated testing pipelines +4. Build a real-world example: an automated scraping system using [Firecrawl](https://firecrawl.dev) in Python +5. Implement best practices for security and efficiency + +By the end, you will have hands-on experience with GitHub Actions and be able to automate your own Python projects effectively. + +> Note: Even though code examples are Python, the concepts and hands-on experience you will gain from the tutorial will apply to any programming language. + +Let's start by understanding the core concepts that make GitHub Actions work. + +## How to Use This GitHub Actions Tutorial + +Before diving into the technical details, here's how to get the most from this GitHub Actions tutorial: + +1. Follow the examples sequentially - each builds on previous concepts +2. Try running the workflows yourself - hands-on practice is crucial +3. Refer back to earlier sections as needed +4. Use the provided code samples as templates for your own projects + +## Understanding GitHub Actions Core Concepts + +To write your own GitHub Actions workflows, you need to understand how its different components work together. Let's break down these core concepts using a practical example: automating tests for a simple Python script. + +### GitHub Actions workflows and their components + +A workflow is an automated process that you define in a YAML file within your repository's `.github/workflows` directory. Think of it as a recipe that tells GitHub exactly what to do, how and when to do it. You can transform virtually any programmable task into a GitHub workflow as long as it can be executed in a Linux, Windows, or macOS environment and doesn't require direct user interaction. + +Here is a basic workflow structure: + +```yaml +# test.yaml +name: Python Tests +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' +``` + +The YAML file starts by specifying the name of the workflow with the `name` field. Immediately after, we specify the events that triggers this workflow. In this example, the workflow automatically executes on each `git push` command and pull request. We will learn more about events and triggers in a later section. + +Next, we define jobs, which are the building blocks of workflows. Each job: + +- Runs on a fresh virtual machine (called a runner) that is specified using the `runs-on` field. +- Can execute multiple steps in sequence +- Can run in parallel with other jobs +- Has access to shared workflow data + +For example, you might have separate jobs for testing and deployment: + +```yaml +jobs: + test: + runs-on: ubuntu-latest + ... + deploy: + runs-on: macos-latest + ... +``` + +Each job can contain one or more `steps` that are executed sequentially. Steps are individual tasks that make up your job. They can: + +- Run commands or shell scripts +- Execute actions (reusable units of code) +- Run commands in Docker containers +- Reference other GitHub repositories + +For example, a typical test job might have steps to: + +1. Check out (clone) code from your GitHub repository +2. Set up dependencies +3. Run tests +4. Upload test results + +Each step can specify: + +- `name`: A display name for the step +- `uses`: Reference to an action to run +- `run`: Any operating-system specific terminal command like `pip install package` or `python script.py` +- `with`: Input parameters for actions +- `env`: Environment variables for the step + +Now that we understand jobs and steps, let's look at Actions - the reusable building blocks that make GitHub Actions so powerful. + +### Actions + +The `test.yaml` file from earlier has a single `test` job that executes two steps: + +1. Checking out the repository code using a built-in `actions/checkout@v3` action. +2. Setting up a Python environment with `actions/setup-python@v4` and `python-version` as an input parameter for said action. + +```bash +# test.yaml +name: Python Tests +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' +``` + +Actions are reusable units of code that can be shared across workflows (this is where GitHub Actions take its name). They are like pre-packaged functions that handle common tasks. For instance, instead of writing code to set up Node.js or caching dependencies, you can use the GitHub official actions like: + +- `actions/setup-node@v3` - Sets up Node.js environment +- `actions/cache@v3` - Caches dependencies and build outputs +- `actions/upload-artifact@v3` - Uploads workflow artifacts +- `actions/download-artifact@v3` - Downloads workflow artifacts +- `actions/labeler@v4` - Automatically labels pull requests +- `actions/stale@v8` - Marks and closes stale issues/PRs +- `actions/dependency-review-action@v3` - Reviews dependency changes + +### Events and triggers + +Events are specific activities that trigger a workflow. Common triggers include: + +- `push`: When code is pushed to the repository +- `pull_request`: When a PR is opened or updated +- `schedule`: At specified times using cron syntax +- `workflow_dispatch`: Manual trigger via GitHub UI + +Here is how you can configure multiple triggers: + +```yaml +name: Comprehensive Workflow +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 0 * * *' # Daily at midnight + workflow_dispatch: # Manual trigger + +jobs: + process: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run daily tasks + run: python daily_tasks.py + env: + API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} +``` + +This example shows how a single workflow can: + +- Run automatically on code changes on `git push` +- Execute daily scheduled tasks with cron +- Be triggered automatically when needed through the GitHub UI +- Handle sensitive data like API keys securely + +### Cron jobs in GitHub Actions + +To use the `schedule` trigger effectively in GitHub Actions, you'll need to understand cron syntax. This powerful scheduling format lets you automate workflows to run at precise times. The syntax uses five fields to specify when a job should run: + +![Cron syntax diagram showing minute, hour, day of month, month, and day of week fields with examples and explanations for GitHub Actions scheduling](github-actions-tutorial-images/cron-syntax.png) + +Here are some common cron schedule examples: + +```yaml +# Daily at 3:30 AM UTC +- cron: '30 3 * * *' + +# Every Monday at 1:00 PM UTC +- cron: '0 13 * * 1' + +# Every 6 hours at the first minute +- cron: '0 */6 * * *' + +# At minute 15 of every hour +- cron: '15 * * * *' + +# Every weekday (Monday through Friday) +- cron: '0 0 * * 1-5' + +# Each day at 12am, 6am, 12pm, 6pm on Tuesday, Thursday, Saturday +- cron: '0 0,6,12,18 * * 1,3,5' +``` + +Here is a sample workflow for a scraping job with four different schedules (multiple schedules are allowed): + +```yaml +name: Price Scraper Schedules +on: + schedule: + - cron: '0 */4 * * *' # Every 4 hours + - cron: '30 1 * * *' # Daily at 1:30 AM UTC + - cron: '0 9 * * 1-5' # Weekdays at 9 AM UTC + +jobs: + scrape: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run Firecrawl scraper + env: + API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} + run: python scraper.py +``` + +Remember that GitHub Actions runs on UTC time, and schedules might experience slight delays during peak GitHub usage. That's why it's helpful to combine `schedule` with `workflow_dispatch` as we saw earlier - giving you both automated and manual trigger options. + +--------------- + +Understanding these core concepts allows you to create workflows that are efficient (running only when needed), secure (properly handling sensitive data), maintainable (using reusable actions) and scalable (running on different platforms). + +In the next section, we will put these concepts into practice by creating your first GitHub actions workflow. + +## Creating Your First GitHub Actions Workflow + +Let's create a practical GitHub Actions workflow from scratch. We'll build a workflow that automatically tests a Python script and runts it on a schedule - a universal task applicable to any programming language. + +### Setting up the environment + +Let's start by creating a working directory for this mini-project: + +```bash +mkdir first-workflows +cd first-workflows +``` + +Let's create the standard `.github/workflows` folder structure GitHub uses for detecting workflow files: + +```bash +mkdir -p .github/workflows +``` + +The workflow files can have any name but must have a `.yml` extension: + +```bash +touch .github/workflows/system_monitor.yml +``` + +In addition to the workflows folder, create a `tests` folder as well as a test file: + +```bash +mkdir tests +touch tests/test_main.py +``` + +We should also create the `main.py` file along with a `requirements.txt`: + +```bash +touch main.py requirements.txt +``` + +Then, add these two dependencies to `requirements.txt`: + +```text +psutil>=5.9.0 +pytest>=7.0.0 +``` + +Finally, let's initialize git and make our first commit: + +```bash +git init +git add . +git commit -m "Initial commit" +``` + +Check out the [Git documentation](https://git-scm.com/doc) if you don't have it installed already. + +### Writing your first workflow file + +Let's write the workflow logic first. Open `system_monitor.yml` and paste each code snippet we are about to define one after the other. + +- Workflow name and triggers: + +```yaml +name: System Monitoring +on: + schedule: + - cron: '*/30 * * * *' # Run every 30 minutes + workflow_dispatch: # Enables manual trigger +``` + +In this part, we give a descriptive name to the workflow that appears in GitHub's UI. Using the `on` field, we set the workflow to run every 30 minutes and through a manual trigger. + +- Job definition: + +```yaml +jobs: + run_script: + runs-on: ubuntu-latest +``` + +`jobs` contains all the jobs in this workflow and it has a `run_script` name, which is a unique identifier. + +- Steps: + +There are five steps that run sequentially in this workflow. They are given descriptive names that appear in the GitHub UI and uses official GitHub actions and custom terminal commands. + +```yaml +jobs: + monitor: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: pytest tests/ + + - name: Collect system metrics + run: python main.py +``` + +Here is what each step does: + +1. Check out repository code with `actions/checkout@v3`. +2. Configures Python 3.9 environment. +3. Runs two terminal commands that: + - Install/upgrade `pip` + - Install `pytest` package +4. Runs the tests located in the `tests` directory using `pytest`. +5. Executes the main script with `python main.py`. + +Notice the use of `|` (pipe) operator for multi-line commands. + +After you complete writing the workflow, commit the changes to Git: + +```bash +git add . +git commit -m "Add a workflow file for monitoring system resources" +``` + +### Creating the Python script + +Now, let's write the `main.py` file, which is a monitoring script that helps software developers track system resource usage over time, enabling them to identify performance bottlenecks and capacity issues in their development environment. + +```python +import psutil +import json +from datetime import datetime +from pathlib import Path +``` + +This script collects and logs system metrics over time. It uses `psutil` to gather CPU usage, memory usage, disk usage, and active process counts. The metrics are timestamped and saved to JSON files organized by date. + +The script has three main functions: + +```python +def get_system_metrics(): + """Collect key system metrics""" + metrics = { + "cpu_percent": psutil.cpu_percent(interval=1), + "memory_percent": psutil.virtual_memory().percent, + "disk_usage": psutil.disk_usage('/').percent, + "timestamp": datetime.now().isoformat() + } + + # Add running processes count + metrics["active_processes"] = len(psutil.pids()) + + return metrics +``` + +`get_system_metrics()` - Collects current system metrics including CPU percentage, memory usage percentage, disk usage percentage, timestamp, and count of active processes. + +```python +def save_metrics(metrics): + """Save metrics to a JSON file with today's date""" + date_str = datetime.now().strftime("%Y-%m-%d") + reports_dir = Path("system_metrics") + reports_dir.mkdir(exist_ok=True) + + # Save to daily file + file_path = reports_dir / f"metrics_{date_str}.json" + + # Load existing metrics if file exists + if file_path.exists(): + with open(file_path) as f: + daily_metrics = json.load(f) + else: + daily_metrics = [] + + # Append new metrics + daily_metrics.append(metrics) + + # Save updated metrics + with open(file_path, 'w') as f: + json.dump(daily_metrics, f, indent=2) +``` + +`save_metrics()` - Handles saving the metrics to JSON files. It creates a `system_metrics` directory if needed, and saves metrics to date-specific files (e.g. `metrics_2024-12-12.json`). If a file for the current date exists, it loads and appends to it, otherwise creates a new file. + +```python +def main(): + try: + metrics = get_system_metrics() + save_metrics(metrics) + print(f"System metrics collected at {metrics['timestamp']}") + print(f"CPU: {metrics['cpu_percent']}% | Memory: {metrics['memory_percent']}%") + return True + except Exception as e: + print(f"Error collecting metrics: {str(e)}") + return False + +if __name__ == "__main__": + main() +``` + +`main()` - Orchestrates the metric collection and saving process. It calls `get_system_metrics()`, saves the data via `save_metrics()`, prints current CPU and memory usage to console, and handles any errors that occur during execution. + +The script can be run directly or imported as a module. When run directly (which is what happens in a GitHub Actions workflow), it executes the `main()` function which collects and saves one set of metrics. + +Combine the code snippets above into the `main.py` file and commit the changes: + +```bash +git add . +git commit -m "Add the main.py functionality" +``` + +### Adding tests + +Testing is a critical part of software engineering workflows for several reasons: + +1. Reliability: Tests help ensure code behaves correctly and consistently across changes. +2. Regression prevention: Tests catch when new changes break existing functionality. +3. Documentation: Tests serve as executable documentation of expected behavior +4. Design feedback: Writing tests helps identify design issues early +5. Confidence: A good test suite gives confidence when refactoring or adding features + +For our system metrics collection script, tests would be valuable to verify: + +- The `get_system_metrics()` function returns data in the expected format with valid ranges +- The `save_metrics()` function properly handles file operations and JSON serialization +- Error handling works correctly for various failure scenarios +- The `main()` function orchestrates the workflow as intended + +With that said, let's work on the `tests/test_main.py` file: + +```python +import json +from datetime import datetime +from pathlib import Path +from main import get_system_metrics, save_metrics +``` + +The test file we are about to write demonstrates key principles of testing with `pytest`, a popular Python testing framework. Pytest makes it easy to write tests by using simple `assert` statements and providing a rich set of features for test organization and execution. The test functions are automatically discovered by `pytest` when their names start with `test_`, and each function tests a specific aspect of the system's functionality. + +```python +def test_get_system_metrics(): + """Test if system metrics are collected correctly""" + metrics = get_system_metrics() + + # Check if all required metrics exist and are valid + assert 0 <= metrics['cpu_percent'] <= 100 + assert 0 <= metrics['memory_percent'] <= 100 + assert metrics['active_processes'] > 0 + assert 'timestamp' in metrics +``` + +In this example, we have two test functions that verify different components of our metrics collection system. The first test, `test_get_system_metrics()`, checks if the metrics collection function returns data in the expected format and with valid ranges. It uses multiple assert statements to verify that CPU and memory percentages are between 0 and 100, that there are active processes, and that a timestamp is included. This demonstrates the practice of testing both the structure of returned data and the validity of its values. + +```python +def test_save_and_read_metrics(): + """Test if metrics are saved and can be read back""" + # Get and save metrics + metrics = get_system_metrics() + save_metrics(metrics) + + # Check if file exists and contains data + date_str = datetime.now().strftime("%Y-%m-%d") + file_path = Path("system_metrics") / f"metrics_{date_str}.json" + + assert file_path.exists() + with open(file_path) as f: + saved_data = json.load(f) + + assert isinstance(saved_data, list) + assert len(saved_data) > 0 +``` + +The second test, `test_save_and_read_metrics()`, showcases integration testing by verifying that metrics can be both saved to and read from a file. It follows a common testing pattern: arrange (setup the test conditions), act (perform the operations being tested), and assert (verify the results). The test ensures that the file is created in the expected location and that the saved data maintains the correct structure. This type of test is particularly valuable as it verifies that different components of the system work together correctly. + +Combine the above code snippets and commit the changes to GitHub: + +```bash +git add . +git commit -m "Write tests for main.py" +``` + +### Running your first GitHub Actions workflow + +Now that we've created our system monitoring workflow, let's set it up on GitHub and run it. First, push everything we have to a new GitHub repository: + +```bash +git remote add origin https://github.com/your-username/your-repository.git +git branch -M main +git push -u origin main +``` + +Once the workflow file is pushed, GitHub automatically detects it and displays in the "Actions" tab of your repository. The workflow is scheduled so you don't need to do anything - the first workflow run will happen within 30 minutes (remember how we set the running interval to `*/30` with cron). Since the workflow also includes a `workflow_dispatch` field, you can trigger it manually by clicking on the "Run workflow" button. + +After clicking the button, a new run appears within a few seconds (refresh if you don't see it). To see the workflow run in real-time, click on it and expand the `monitor` job. You'll see each step executing: + +- Checking out repository +- Setting up Python +- Installing dependencies +- Running tests +- Collecting metrics + +### Committing changes made by GitHub Actions workflows + +Right now, our workflow file has a problem - while it successfully collects metrics, it doesn't commit and push the changes back to the repository. This means that although metrics are being gathered, they aren't being saved in version control. Let's modify the workflow to automatically commit and push the collected metrics: + +```yaml +name: System Monitor + +on: + schedule: + - cron: '*/30 * * * *' + workflow_dispatch: + +permissions: + contents: write + +jobs: + monitor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: python -m pytest + + - name: Collect metrics + run: python main.py + + - name: Commit and push changes + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add metrics.json + git commit -m "Update metrics" || exit 0 + git push +``` + +The key changes in this updated workflow are: + +1. Added `permissions` block with `contents: write` to allow the workflow to push changes back to the repository. +2. Added a new "Commit and push changes" step that: + - Configures git user identity as `github-actions` bot + - Stages the `metrics.json` file + - Creates a commit with message "Update metrics" + - Pushes the changes back to the repository + +The "|| exit 0" after `git commit` ensures the workflow doesn't fail if there are no changes to commit. + +This allows the workflow to automatically save and version control the metrics it collects. Let's commit the changes to the workflow and push: + +```bash +git add . +git commit -m "Add a commit step to the workflow file" +git push origin main +``` + +After this change, try running the workflow manually and verify its success by navigating to the Actions tab in your GitHub repository. You should see the workflow run and the `metrics.json` file updated with new data. + +## Managing Sensitive Data and Environment Variables + +When building automated workflows with GitHub Actions, proper handling of sensitive data like API keys, passwords, and access tokens is crucial for security. Let's explore best practices for managing these credentials. + +### Understanding environment variables in GitHub Actions + +Environment variables in GitHub Actions can be set at different levels: + +- Repository level (GitHub Secrets) +- Workflow level +- Job level +- Step level + +Here is how to properly configure and use them: + +#### 1. Setting up the repository secrets + +First, store sensitive values as repository secrets: + +1. Navigate to your GitHub repository +2. Go to Settings → Secrets and variables → Actions +3. Click "New repository secret" +4. Add your secrets with descriptive names like: + +- `API_KEY` +- `DATABASE_URL` +- `AWS_ACCESS_KEY` + +#### 2. Using secrets in workflows + +Reference secrets in your workflow file using the `secrets` context: + +```yaml +name: Web Scraping Pipeline +# ... the rest of the file + +jobs: + scrape: + runs-on: ubuntu-latest + + steps: + # ... the rest of the steps + + - name: Run scraper + env: + API_KEY: ${{ secrets.API_KEY }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + run: python scraper.py +``` + +Above, the "Run scraper" step executes `scraper.py` which relies on two environment variables configured through the `secrets` context. + +#### 3. Local development with .env files + +For local development, use `.env` files to manage environment variables: + +```bash +touch .env +echo "API_KEY='your-api-key-here'" >> .env +echo "DATABASE_URL='postgresql://user:pass@localhost:5432/db'" >> .env +``` + +Create a `.gitignore` file to prevent committing sensitive data: + +```bash +echo ".env" >> .gitignore +``` + +#### 4. Loading environment variables in Python + +Use `python-dotenv` to load variables from `.env` files: + +```python +from dotenv import load_dotenv +import os + +# Load environment variables from .env file +load_dotenv() + +# Access variables +api_key = os.getenv('API_KEY') +database_url = os.getenv('DATABASE_URL') + +if not api_key or not database_url: + raise ValueError("Missing required environment variables") +``` + +This code demonstrates loading environment variables from a `.env` file using `python-dotenv`. The `load_dotenv()` function reads the variables, which can then be accessed via `os.getenv()`. Basic validation ensures required variables exist. + +#### 5. Environment variable validation + +Create a configuration class to validate environment variables: + +```python +from pydantic import BaseSettings, SecretStr + + +class Settings(BaseSettings): + api_key: SecretStr + database_url: str + debug_mode: bool = False + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +# Initialize settings +settings = Settings() +``` + +This approach using Pydantic provides several advantages for environment variable management: + +1. Type validation - Pydantic automatically validates types and converts values +2. Default values - The `debug_mode` demonstrates setting defaults +3. Secret handling - `SecretStr` provides secure handling of sensitive values +4. Centralized config - All environment variables are defined in one place +5. IDE support - Get autocomplete and type hints when using the settings object + +The `Settings` class inherits from `BaseSettings` which automatically loads from environment variables. The `Config` class specifies to also load from a `.env` file. + +Using `settings = Settings()` creates a validated configuration object that can be imported and used throughout the application. This is more robust than accessing `os.environ` directly. + +Example usage: + +```python +settings.api_key.get_secret_value() # Securely access API key +settings.database_url # Type-checked database URL +settings.debug_mode # Boolean with default value +``` + +#### 6. Handle different environments + +Handle different environments (development, staging, production) using environment-specific files: + +```bash +.env # Default environment variables +.env.development # Development-specific variables +.env.staging # Staging-specific variables +.env.production # Production-specific variables +``` + +Load the appropriate file based on the environment: + +```bash +from dotenv import load_dotenv +import os + +env = os.getenv('ENVIRONMENT', 'development') +env_file = f'.env.{env}' + +load_dotenv(env_file) +``` + +This approach allows you to maintain separate configurations for different environments while keeping sensitive information secure. The environment-specific files can contain different values for the same variables, such as: + +- Development environment may use local services and dummy credentials +- Staging environment may use test services with restricted access +- Production environment contains real credentials and production service endpoints + +You can also combine this with the `Pydantic` settings approach shown earlier for robust configuration management across environments. + +For example, staging might use a test database while production uses the live database: + +```bash +# .env.staging: +DATABASE_URL=postgresql://test-db.example.com +API_KEY=test-key +``` + +```bash +.env.production: +DATABASE_URL=postgresql://prod-db.example.com +API_KEY=live-key +``` + +This separation helps prevent accidental use of production resources during development and testing. + +## Building Real-World Python Workflows + +Let's explore three practical examples of GitHub Actions workflows for common Python tasks: web scraping, package publishing, and container builds. + +### 1. Scheduled web scraping with Firecrawl + +Web scraping is a common use case for automated workflows. Let's build a workflow that scrapes [Hacker News](https://news.ycombinator.com/) on a schedule using [Firecrawl](https://docs.firecrawl.dev), which is a Python AI-based web scraping engine designed for large-scale data collection. Here are some key benefits that make Firecrawl an excellent choice for this task: + +1. **Enterprise-grade automation and scalability** - Firecrawl streamlines web scraping with powerful automation features. +2. **AI-powered content extraction** - Maintains scraper reliability over time by identifying and extracting data based on semantic descriptions instead of relying HTML elements and CSS selectors. +3. **Handles complex scraping challenges** - Automatically manages proxies, anti-bot mechanisms, and dynamic JavaScript content. +4. **Multiple output formats** - Supports scraping and converting data in markdown, tabular, screenshots, and HTML, making it versatile for various applications. +5. **Built-in rate limiting and request management** - Ensures efficient and compliant data extraction. +6. **Geographic location customization** - Avoids IP bans by customizing the geographic location of requests. + +Let's build our web scraping workflow using Firecrawl to demonstrate these capabilities. + +```bash +# Create project directory and install dependencies +mkdir hacker-news-scraper && cd hacker-news-scraper +pip install firecrawl-py pydantic python-dotenv + +# Create necessary files +touch requirements.txt scraper.py .env + +# Add dependencies to requirements.txt +echo "firecrawl-py\npydantic\npython-dotenv" > requirements.txt + +# Add Firecrawl API key to .env (get your key at firecrawl.dev/signin/signup) +echo "FIRECRAWL_API_KEY='your_api_key_here'" > .env +``` + +Open the scraper script where we define our scraping logic: + +```python +# scraper.py +import json +from firecrawl import FirecrawlApp +from dotenv import load_dotenv +from pydantic import BaseModel, Field +from typing import List +from datetime import datetime + +load_dotenv() + +BASE_URL = "https://news.ycombinator.com/" +``` + +First, we import necessary libraries and packages, also defining a base URL we are going to scrape. + +```python +class NewsItem(BaseModel): + title: str = Field(description="The title of the news item") + source_url: str = Field(description="The URL of the news item") + author: str = Field( + description="The URL of the post author's profile concatenated with the base URL." + ) + rank: str = Field(description="The rank of the news item") + upvotes: str = Field(description="The number of upvotes of the news item") + date: str = Field(description="The date of the news item.") + + +class NewsData(BaseModel): + news_items: List[NewsItem] +``` + +We define two Pydantic models to structure our scraped data: + +1. `NewsItem` - Represents a single news item with fields for title, URL, author, rank, upvotes and date +2. `NewsData` - Contains a list of `NewsItem` objects + +These models help validate the scraped data and ensure it matches our expected schema. They also make it easier to serialize/deserialize the data when saving to JSON. Using `Field` with a detailed description is crucial because Firecrawl uses these definitions to automatically detect the HTMl elements and CSS selectors we are looking for. + +```python +def get_news_data(): + app = FirecrawlApp() + + data = app.scrape_url( + BASE_URL, + params={ + "formats": ["extract"], + "extract": {"schema": NewsData.model_json_schema()}, + }, + ) + + return data +``` + +The `get_news_data()` function uses Firecrawl to scrape Hacker News. It creates a `FirecrawlApp` instance and calls `scrape_url()` with the `BASE_URL` and parameters specifying we want to extract data according to our `NewsData` schema. The schema helps Firecrawl automatically identify and extract the relevant HTML elements. The function returns the scraped data containing news items with their titles, URLs, authors, ranks, upvotes and dates. + +```python +def save_firecrawl_news_data(): + """ + Save the scraped news data to a JSON file with the current date in the filename. + """ + # Get the data + data = get_news_data() + # Format current date for filename + date_str = datetime.now().strftime("%Y_%m_%d_%H_%M") + filename = f"firecrawl_hacker_news_data_{date_str}.json" + + # Save the news items to JSON file + with open(filename, "w") as f: + json.dump(data["extract"]["news_items"], f, indent=4) + + print(f"{datetime.now()}: Successfully saved the news data.") + +if __name__ == "__main__": + save_firecrawl_news_data() + +``` + +The `save_firecrawl_news_data()` function handles saving the scraped Hacker News data to a JSON file. It first calls `get_news_data()` to fetch the latest data from Hacker News. Then it generates a filename using the current timestamp to ensure uniqueness. The data is saved to a JSON file with that filename, with the news items formatted with proper indentation for readability. Finally, it prints a confirmation message with the current timestamp when the save is complete. This function provides a convenient way to store snapshots of Hacker News data that can be analyzed later. + +Combine these snippets into the `scraper.py` script. Then, we can write a workflow that executes it on schedule: + +```bash +cd .. # Change back to the project root directory +touch .github/workflows/hacker-news-scraper.py # Create the workflow file +``` + +Here is what the workflow file must look like: + +```yaml +name: Run Hacker News Scraper + +permissions: + contents: write + +on: + schedule: + - cron: "0 */6 * * *" + workflow_dispatch: + +jobs: + scrape: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r hacker-news-scraper/requirements.txt + + - name: Run scraper + run: python hacker-news-scraper/scraper.py + + - name: Commit and push if changes + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add . + git commit -m "Update scraped data" -a || exit 0 + git push +``` + +This workflow runs our Hacker News scraper every 6 hours using GitHub Actions. It sets up Python, installs dependencies, executes the scraper, and automatically commits any new data to the repository. The workflow can also be triggered manually using the `workflow_dispatch` event. One important note about the paths specified in the workflow file is that they must match your repository's directory structure exactly, including the requirements.txt location and the path to your scraper script. + +To enable the workflow, simply push all the changes to GitHub and test it through the UI. The next runs will be automatic. + +```bash +git add . +git commit -m "Add a scraping workflow" +git push origin main +``` + +### 2. Package publishing workflow + +Publishing Python packages to PyPI (Python Package Index) typically involves several steps. First, developers need to prepare their package by creating a proper directory structure, writing setup files, and ensuring all metadata is correct. Then, the package needs to be built into distribution formats - both source distributions (`sdist`) and wheel distributions (`bdist_wheel`). Finally, these distribution files are uploaded to PyPI using tools like `twine`. This process often requires careful version management and proper credentials for the package repository. While this can be done manually, automating it with CI/CD pipelines like GitHub Actions ensures consistency and reduces human error in the release process. + +For example, the following workflow publishes a new version of a package when you create a new release: + +```yaml +name: Publish Python Package +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python -m twine upload dist/* +``` + +The workflow automates publishing Python packages to PyPI when GitHub releases are created. + +Required setup steps: + +1. Package must have `setup.py` or `pyproject.toml` configured +2. Create PyPI account at `pypi.org` +3. Generate PyPI API token with upload permissions +4. Store token as `PYPI_API_TOKEN` in repository secrets + +The workflow process: + +1. Triggers on new GitHub release +2. Checks out code and sets up Python +3. Installs build tools +4. Creates distribution packages +5. Uploads to PyPI using stored token + +The `__token__` username is used with PyPI's token authentication, while the actual token is accessed securely through GitHub secrets. + +### 3. Container build and push workflow + +GitHub Actions can also automate building and pushing Docker containers to container registries like Docker Hub or GitHub Container Registry (GHCR). This workflow is useful for maintaining containerized applications and ensuring your latest code changes are packaged into updated container images. + +The process typically involves: + +1. Building a Docker image from your `Dockerfile` +2. Tagging the image with version/metadata +3. Authenticating with the container registry +4. Pushing the tagged image to the registry + +This automation ensures your container images stay in sync with code changes and are readily available for deployment. Here is a sample workflow containing these steps: + +```yaml +name: Build and Push Container +on: + push: + branches: [main] + paths: + - 'Dockerfile' + - 'src/**' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: | + user/app:latest + user/app:${{ github.sha }} +``` + +This workflow introduces a few new GitHub Actions concepts and syntax: + +The `paths` trigger filter ensures the workflow only runs when changes are made to the Dockerfile or files in the `src` directory, preventing unnecessary builds. + +`docker/setup-buildx-action` configures Docker Buildx, which provides enhanced build capabilities including multi-platform builds and build caching. + +`docker/login-action` handles registry authentication. Before using this, you must: + +1. [Create a Docker Hub account](https://app.docker.com/signup) +2. Generate an access token in Docker Hub settings +3. Add `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` as repository secrets in GitHub + +`docker/build-push-action` is a specialized action for building and pushing Docker images. The configuration shows: + +- `context: .` (builds from current directory) +- `push: true` (automatically pushes after building) +- `tags:` specifies multiple tags including: + - `latest:` rolling tag for most recent version + - `github.sha:` unique tag using commit hash for versioning + +The workflow assumes you have: + +- A valid Dockerfile in your repository +- Required application code in `src` directory +- Docker Hub repository permissions +- Properly configured repository secrets + +When this workflow runs successfully, it produces a containerized version of your application that is automatically published to Docker Hub and can be pulled with either the latest tag or specific commit hash. + +## Understanding How GitHub Actions Run + +When you trigger a GitHub Actions run, whether manually or through automated events, the platform: + +1. Provisions a fresh virtual machine (runner) +2. Executes your workflow steps sequentially +3. Reports results back to GitHub +4. Tears down the runner environment + +This isolated execution model ensures consistency and security for each GitHub Actions run. + +## Glossary of GitHub Actions Terms + +- **GitHub Actions**: GitHub's built-in automation platform for software development workflows +- **GitHub Actions Workflow**: A configurable automated process made up of one or more jobs +- **GitHub Actions Jobs**: Individual units of work that can run sequentially or in parallel +- **GitHub Actions Run**: A single execution instance of a workflow +- **GitHub Actions Environment Variables**: Configuration values available during workflow execution +- **GitHub Actions Secrets**: Encrypted environment variables for sensitive data +- **GitHub Actions Tutorial**: A guide teaching the fundamentals of GitHub Actions (like this one!) + +## Conclusion + +Throughout this tutorial, we've explored the fundamentals and practical applications of GitHub Actions for Python development. From understanding core concepts like workflows, jobs, and actions, to implementing real-world examples including automated testing, web scraping, package publishing, and container builds, you've gained hands-on experience with this powerful automation platform. We've also covered critical aspects like managing sensitive data through environment variables and secrets, ensuring your automated workflows are both secure and maintainable. + +As you continue your journey with GitHub Actions, remember that automation is an iterative process. Start small with basic workflows, test thoroughly, and gradually add complexity as needed. The examples provided here serve as templates that you can adapt and expand for your specific use cases. For further learning, explore the [GitHub Actions documentation](https://docs.github.com/en/actions), join the [GitHub Community Forum](https://github.community/), and experiment with the vast ecosystem of pre-built actions available in the [GitHub Marketplace](https://github.com/marketplace?type=actions). Whether you're building a personal project or managing enterprise applications, GitHub Actions provides the tools you need to streamline your development workflow and focus on what matters most - writing great code. + +If you want to learn more about Firecrawl, the web scraping API we used today, you can read the following posts: + +- [Guide to Scheduling Web Scrapers in Python](https://www.firecrawl.dev/blog/automated-web-scraping-free-2025) +- [Mastering Firecrawl's Scrape Endpoint](https://www.firecrawl.dev/blog/mastering-firecrawl-scrape-endpoint) +- [Getting Started With Predicted Outputs in OpenAI](https://www.firecrawl.dev/blog/getting-started-with-predicted-outputs-openai) + +Thank you for reading! From d8847bb4ceb63db4bb6cc1aa2d6d1a272acd179c Mon Sep 17 00:00:00 2001 From: rafaelmmiller <150964962+rafaelsideguide@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:34:50 -0300 Subject: [PATCH 05/52] fixes schema warning --- apps/python-sdk/example.py | 2 +- apps/python-sdk/firecrawl/firecrawl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/python-sdk/example.py b/apps/python-sdk/example.py index 686b7676..fb960187 100644 --- a/apps/python-sdk/example.py +++ b/apps/python-sdk/example.py @@ -59,7 +59,7 @@ class ArticleSchema(BaseModel): commentsURL: str class TopArticlesSchema(BaseModel): - top: List[ArticleSchema] = Field(..., max_items=5, description="Top 5 stories") + top: List[ArticleSchema] = Field(..., description="Top 5 stories") llm_extraction_result = app.scrape_url('https://news.ycombinator.com', { 'formats': ['extract'], diff --git a/apps/python-sdk/firecrawl/firecrawl.py b/apps/python-sdk/firecrawl/firecrawl.py index 59130784..45ed27d8 100644 --- a/apps/python-sdk/firecrawl/firecrawl.py +++ b/apps/python-sdk/firecrawl/firecrawl.py @@ -27,7 +27,7 @@ class FirecrawlApp: Parameters for the extract operation. """ prompt: str - schema: Optional[Any] = None + schema_: Optional[Any] = pydantic.Field(None, alias='schema') system_prompt: Optional[str] = None allow_external_links: Optional[bool] = False From ff878bc6f537e64769dc1cfe03e5629c28aab7dc Mon Sep 17 00:00:00 2001 From: rafaelmmiller <150964962+rafaelsideguide@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:35:22 -0300 Subject: [PATCH 06/52] bump version --- apps/python-sdk/firecrawl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/python-sdk/firecrawl/__init__.py b/apps/python-sdk/firecrawl/__init__.py index 207312b0..31d68095 100644 --- a/apps/python-sdk/firecrawl/__init__.py +++ b/apps/python-sdk/firecrawl/__init__.py @@ -13,7 +13,7 @@ import os from .firecrawl import FirecrawlApp # noqa -__version__ = "1.6.3" +__version__ = "1.6.4" # Define the logger for the Firecrawl project logger: logging.Logger = logging.getLogger("firecrawl") From fe6b003fcf1191055bbc73ea379f6a47772d1c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Mon, 9 Dec 2024 18:49:48 +0100 Subject: [PATCH 07/52] fix(js-sdk/batchScrapeUrls): zod support --- apps/js-sdk/firecrawl/package-lock.json | 4 ++-- apps/js-sdk/firecrawl/src/index.ts | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/js-sdk/firecrawl/package-lock.json b/apps/js-sdk/firecrawl/package-lock.json index 81a4a146..ff78d90e 100644 --- a/apps/js-sdk/firecrawl/package-lock.json +++ b/apps/js-sdk/firecrawl/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mendable/firecrawl-js", - "version": "1.4.4", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mendable/firecrawl-js", - "version": "1.4.4", + "version": "1.9.1", "license": "MIT", "dependencies": { "axios": "^1.6.8", diff --git a/apps/js-sdk/firecrawl/src/index.ts b/apps/js-sdk/firecrawl/src/index.ts index 248443d0..eb402b4d 100644 --- a/apps/js-sdk/firecrawl/src/index.ts +++ b/apps/js-sdk/firecrawl/src/index.ts @@ -577,7 +577,24 @@ export default class FirecrawlApp { webhook?: CrawlParams["webhook"], ): Promise { const headers = this.prepareHeaders(idempotencyKey); - let jsonData: any = { urls, ...(params ?? {}), webhook }; + let jsonData: any = { urls, ...params }; + if (jsonData?.extract?.schema) { + let schema = jsonData.extract.schema; + + // Try parsing the schema as a Zod schema + try { + schema = zodToJsonSchema(schema); + } catch (error) { + + } + jsonData = { + ...jsonData, + extract: { + ...jsonData.extract, + schema: schema, + }, + }; + } try { const response: AxiosResponse = await this.postRequest( this.apiUrl + `/v1/batch/scrape`, From 6776aee1c36b5be2c1d773775396ff5b0f9ea1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Mon, 9 Dec 2024 19:29:32 +0100 Subject: [PATCH 08/52] feat(auth): extend rate limiter logging to make it easier to debug --- apps/api/src/controllers/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index 1b94fb01..8f4d49ea 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -270,7 +270,7 @@ export async function supaAuthenticateUser( try { await rateLimiter.consume(team_endpoint_token); } catch (rateLimiterRes) { - logger.error(`Rate limit exceeded: ${rateLimiterRes}`, { teamId }); + logger.error(`Rate limit exceeded: ${rateLimiterRes}`, { teamId, priceId, plan: subscriptionData?.plan, mode, rateLimiterRes }); const secs = Math.round(rateLimiterRes.msBeforeNext / 1000) || 1; const retryDate = new Date(Date.now() + rateLimiterRes.msBeforeNext); From 91a1a9a1fc006dbb61b09e2ccc74f39d8890ae02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Mon, 9 Dec 2024 19:29:42 +0100 Subject: [PATCH 09/52] fix(crawl-redis/lockURL): reduce logging --- apps/api/src/lib/crawl-redis.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index e61f4892..5cc4d6a4 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -151,7 +151,6 @@ export async function lockURL(id: string, sc: StoredCrawl, url: string): Promise url = normalizeURL(url, sc); logger = logger.child({ url }); - logger.debug("Locking URL " + JSON.stringify(url) + "..."); await redisConnection.sadd("crawl:" + id + ":visited_unique", url); await redisConnection.expire("crawl:" + id + ":visited_unique", 24 * 60 * 60, "NX"); @@ -160,14 +159,14 @@ export async function lockURL(id: string, sc: StoredCrawl, url: string): Promise res = (await redisConnection.sadd("crawl:" + id + ":visited", url)) !== 0 } else { const permutations = generateURLPermutations(url).map(x => x.href); - logger.debug("Adding URL permutations for URL " + JSON.stringify(url) + "...", { permutations }); + // logger.debug("Adding URL permutations for URL " + JSON.stringify(url) + "...", { permutations }); const x = (await redisConnection.sadd("crawl:" + id + ":visited", ...permutations)); res = x === permutations.length; } await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); - logger.debug("lockURL final result: " + res, { res }); + logger.debug("Locking URL " + JSON.stringify(url) + "... result: " + res, { res }); return res; } From a47e278c97d3bed79ede81fca37ec7fff7f9e2bd Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 9 Dec 2024 19:25:48 -0300 Subject: [PATCH 10/52] Nick: bump node sdk --- apps/api/requests.http | 1 + apps/js-sdk/firecrawl/package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/requests.http b/apps/api/requests.http index 0e3b9206..e9259dbc 100644 --- a/apps/api/requests.http +++ b/apps/api/requests.http @@ -69,6 +69,7 @@ content-type: application/json { "urls": ["firecrawl.dev"], "prompt": "What is the title, description and main product of the page?", + "dev.to", "schema": { "title": "string", "description": "string", diff --git a/apps/js-sdk/firecrawl/package.json b/apps/js-sdk/firecrawl/package.json index 73eabc2a..73224dc4 100644 --- a/apps/js-sdk/firecrawl/package.json +++ b/apps/js-sdk/firecrawl/package.json @@ -1,6 +1,6 @@ { "name": "@mendable/firecrawl-js", - "version": "1.9.1", + "version": "1.9.2", "description": "JavaScript SDK for Firecrawl API", "main": "dist/index.js", "types": "dist/index.d.ts", From 4dbe0e6236de98b9f2333ebf669d25e965cc3e16 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 9 Dec 2024 19:26:33 -0300 Subject: [PATCH 11/52] Update requests.http --- apps/api/requests.http | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/requests.http b/apps/api/requests.http index e9259dbc..0e3b9206 100644 --- a/apps/api/requests.http +++ b/apps/api/requests.http @@ -69,7 +69,6 @@ content-type: application/json { "urls": ["firecrawl.dev"], "prompt": "What is the title, description and main product of the page?", - "dev.to", "schema": { "title": "string", "description": "string", From 877f072e3c51e8fe11258ff2635dc993692eeb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Mon, 9 Dec 2024 23:40:44 +0100 Subject: [PATCH 12/52] feat: crawl log parser (poc) --- apps/api/logview.js | 23 +++++++++++++++++++++++ apps/api/src/services/queue-worker.ts | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 apps/api/logview.js diff --git a/apps/api/logview.js b/apps/api/logview.js new file mode 100644 index 00000000..17032b2e --- /dev/null +++ b/apps/api/logview.js @@ -0,0 +1,23 @@ +const fs = require("fs"); + +const logs = fs.readFileSync("log-20780c8a-52f5-4af7-ac48-62997d11ec9b.log", "utf8") + .split("\n").filter(x => x.trim().length > 0).map(x => JSON.parse(x)); + +const crawlIds = [...new Set(logs.map(x => x.crawlId).filter(x => x))]; + +const urlFilter = x => new URL(x).pathname.slice(1) || "root" + +for (const crawlId of crawlIds) { + const crawlLogs = logs.filter(x => x.crawlId === crawlId); + + const jobAdds = crawlLogs.filter(x => x.jobPriority !== undefined && x.message.startsWith("Added job for URL ")); + const jobStarts = crawlLogs.filter(x => x.message.startsWith("🐂 Worker taking job")); + + fs.writeFileSync(crawlId + ".md", + "```mermaid\nflowchart LR\n " + + jobStarts.map(x => `${x.jobId}[${urlFilter(x.url)}]`).join("\n ") + "\n " + + jobAdds.map(x => `${x.jobId}[${urlFilter(jobStarts.find(y => y.jobId === x.jobId).url)}] --> ${x.newJobId}[${urlFilter(x.url)}]`).join("\n ") + + "\n```\n\nURLs scraped: (" + jobStarts.length + ")\n" + + jobStarts.map(x => "- " + x.url).join("\n") + ); +} diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index 78578395..9d25848b 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -346,7 +346,7 @@ workerFun(getScrapeQueue(), processJobInternal); async function processJob(job: Job & { id: string }, token: string) { const logger = _logger.child({ module: "queue-worker", method: "processJob", jobId: job.id, scrapeId: job.id, crawlId: job.data?.crawl_id ?? undefined }); - logger.info(`🐂 Worker taking job ${job.id}`); + logger.info(`🐂 Worker taking job ${job.id}`, { url: job.data.url }); // Check if the job URL is researchhub and block it immediately // TODO: remove this once solve the root issue @@ -505,7 +505,7 @@ async function processJob(job: Job & { id: string }, token: string) { ); await addCrawlJob(job.data.crawl_id, jobId); - logger.debug("Added job for URL " + JSON.stringify(link), { jobPriority, url: link }); + logger.debug("Added job for URL " + JSON.stringify(link), { jobPriority, url: link, newJobId: jobId }); } else { logger.debug("Could not lock URL " + JSON.stringify(link), { url: link }); } From 468b8cdeb9a4bcb773aeceef6e67653f286f6698 Mon Sep 17 00:00:00 2001 From: rafaelmmiller <150964962+rafaelsideguide@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:29:36 -0300 Subject: [PATCH 13/52] removing microsoft from blocklist --- apps/api/src/services/queue-worker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index 78578395..fcb1a7fb 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -354,8 +354,7 @@ async function processJob(job: Job & { id: string }, token: string) { job.data.url && (job.data.url.includes("researchhub.com") || job.data.url.includes("ebay.com") || - job.data.url.includes("youtube.com") || - job.data.url.includes("microsoft.com")) + job.data.url.includes("youtube.com")) ) { logger.info(`🐂 Blocking job ${job.id} with URL ${job.data.url}`); const data = { From 85cbfbb5bb699766b88b139c74837d862cbcb41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Tue, 10 Dec 2024 21:12:31 +0100 Subject: [PATCH 14/52] fix(crawl): disable smart wait This increases the reliability/deterministic-ness of crawls. --- apps/api/logview.js | 7 +++++-- apps/api/src/controllers/v0/crawl.ts | 1 + apps/api/src/controllers/v1/batch-scrape.ts | 2 +- apps/api/src/controllers/v1/crawl.ts | 4 +++- .../api/src/scraper/scrapeURL/engines/fire-engine/index.ts | 1 + .../src/scraper/scrapeURL/engines/fire-engine/scrape.ts | 1 + apps/api/src/scraper/scrapeURL/index.ts | 2 ++ 7 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/api/logview.js b/apps/api/logview.js index 17032b2e..232d2cda 100644 --- a/apps/api/logview.js +++ b/apps/api/logview.js @@ -1,6 +1,6 @@ const fs = require("fs"); -const logs = fs.readFileSync("log-20780c8a-52f5-4af7-ac48-62997d11ec9b.log", "utf8") +const logs = fs.readFileSync("7a373219-0eb4-4e47-b2df-e90e12afd5c1.log", "utf8") .split("\n").filter(x => x.trim().length > 0).map(x => JSON.parse(x)); const crawlIds = [...new Set(logs.map(x => x.crawlId).filter(x => x))]; @@ -9,15 +9,18 @@ const urlFilter = x => new URL(x).pathname.slice(1) || "root" for (const crawlId of crawlIds) { const crawlLogs = logs.filter(x => x.crawlId === crawlId); + fs.writeFileSync("crawl-" + crawlId + ".log", crawlLogs.map(x => JSON.stringify(x)).join("\n")); const jobAdds = crawlLogs.filter(x => x.jobPriority !== undefined && x.message.startsWith("Added job for URL ")); const jobStarts = crawlLogs.filter(x => x.message.startsWith("🐂 Worker taking job")); + const ttl = [...new Set(crawlLogs.filter(x => x.method === "lockURL" && x.res !== undefined).map(x => x.url))] fs.writeFileSync(crawlId + ".md", "```mermaid\nflowchart LR\n " + jobStarts.map(x => `${x.jobId}[${urlFilter(x.url)}]`).join("\n ") + "\n " + jobAdds.map(x => `${x.jobId}[${urlFilter(jobStarts.find(y => y.jobId === x.jobId).url)}] --> ${x.newJobId}[${urlFilter(x.url)}]`).join("\n ") + "\n```\n\nURLs scraped: (" + jobStarts.length + ")\n" - + jobStarts.map(x => "- " + x.url).join("\n") + + jobStarts.map(x => "- " + x.url).join("\n") + "\n\nURLs tried to lock: (" + ttl.length + ")\n" + + ttl.map(x => "- " + x + " ("+ crawlLogs.filter(y => y.method === "lockURL" && y.res !== undefined && y.url === x).length + "; " + crawlLogs.filter(y => y.method === "lockURL" && y.res === true && y.url === x).length + ")").join("\n") ); } diff --git a/apps/api/src/controllers/v0/crawl.ts b/apps/api/src/controllers/v0/crawl.ts index 5de5eccf..06a86f92 100644 --- a/apps/api/src/controllers/v0/crawl.ts +++ b/apps/api/src/controllers/v0/crawl.ts @@ -137,6 +137,7 @@ export async function crawlController(req: Request, res: Response) { await logCrawl(id, team_id); const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions(pageOptions, undefined, undefined); + internalOptions.disableSmartWaitCache = true; // NOTE: smart wait disabled for crawls to ensure contentful scrape, speed does not matter delete (scrapeOptions as any).timeout; diff --git a/apps/api/src/controllers/v1/batch-scrape.ts b/apps/api/src/controllers/v1/batch-scrape.ts index 133977be..064ee73b 100644 --- a/apps/api/src/controllers/v1/batch-scrape.ts +++ b/apps/api/src/controllers/v1/batch-scrape.ts @@ -43,7 +43,7 @@ export async function batchScrapeController( const sc: StoredCrawl = req.body.appendToId ? await getCrawl(req.body.appendToId) as StoredCrawl : { crawlerOptions: null, scrapeOptions: req.body, - internalOptions: {}, + internalOptions: { disableSmartWaitCache: true }, // NOTE: smart wait disabled for batch scrapes to ensure contentful scrape, speed does not matter team_id: req.auth.team_id, createdAt: Date.now(), plan: req.auth.plan, diff --git a/apps/api/src/controllers/v1/crawl.ts b/apps/api/src/controllers/v1/crawl.ts index 08630bfa..3db518d0 100644 --- a/apps/api/src/controllers/v1/crawl.ts +++ b/apps/api/src/controllers/v1/crawl.ts @@ -79,7 +79,7 @@ export async function crawlController( originUrl: req.body.url, crawlerOptions: toLegacyCrawlerOptions(crawlerOptions), scrapeOptions, - internalOptions: {}, + internalOptions: { disableSmartWaitCache: true }, // NOTE: smart wait disabled for crawls to ensure contentful scrape, speed does not matter team_id: req.auth.team_id, createdAt: Date.now(), plan: req.auth.plan, @@ -122,6 +122,7 @@ export async function crawlController( plan: req.auth.plan, crawlerOptions, scrapeOptions, + internalOptions: sc.internalOptions, origin: "api", crawl_id: id, sitemapped: true, @@ -162,6 +163,7 @@ export async function crawlController( team_id: req.auth.team_id, crawlerOptions, scrapeOptions: scrapeOptionsSchema.parse(scrapeOptions), + internalOptions: sc.internalOptions, plan: req.auth.plan!, origin: "api", crawl_id: id, diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index 106803ac..ae953c1b 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -88,6 +88,7 @@ export async function scrapeURLWithFireEngineChromeCDP(meta: Meta): Promise Date: Tue, 10 Dec 2024 21:43:00 +0100 Subject: [PATCH 15/52] feat(scrapeURL/pdf): extend amount of time we're willing to wait for PDFs in crawl/batch scrape mode --- apps/api/src/scraper/scrapeURL/engines/pdf/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts index bdc916e0..19f0a7a8 100644 --- a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts @@ -62,7 +62,7 @@ async function scrapePDFWithLlamaParse(meta: Meta, tempFilePath: string): Promis schema: z.object({ markdown: z.string(), }), - tryCount: 32, + tryCount: meta.options.timeout !== undefined ? 32 : 1200, // 5 minutes if timeout not specified tryCooldown: 250, }); From ce460a3a5619c4797e89c62d66b3a41140931eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Tue, 10 Dec 2024 22:33:53 +0100 Subject: [PATCH 16/52] fix(v1/crawl/status): completed more than total if some scrape jobs fail or are discarded --- apps/api/src/lib/crawl-redis.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index 5cc4d6a4..842f6ebf 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -53,12 +53,15 @@ export async function addCrawlJobs(id: string, job_ids: string[]) { await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); } -export async function addCrawlJobDone(id: string, job_id: string) { +export async function addCrawlJobDone(id: string, job_id: string, success: boolean) { _logger.debug("Adding done crawl job to Redis...", { jobId: job_id, module: "crawl-redis", method: "addCrawlJobDone", crawlId: id }); await redisConnection.sadd("crawl:" + id + ":jobs_done", job_id); - await redisConnection.rpush("crawl:" + id + ":jobs_done_ordered", job_id); await redisConnection.expire("crawl:" + id + ":jobs_done", 24 * 60 * 60, "NX"); - await redisConnection.expire("crawl:" + id + ":jobs_done_ordered", 24 * 60 * 60, "NX"); + + if (success) { + await redisConnection.rpush("crawl:" + id + ":jobs_done_ordered", job_id); + await redisConnection.expire("crawl:" + id + ":jobs_done_ordered", 24 * 60 * 60, "NX"); + } } export async function getDoneJobsOrderedLength(id: string): Promise { From d9e017e5e20d2833a32681c6aabd7b33b26de05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Tue, 10 Dec 2024 22:34:26 +0100 Subject: [PATCH 17/52] feat(queue-worker/crawl): solidify redirect behaviour --- apps/api/src/services/queue-worker.ts | 29 +++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index 527bfc83..74e954cd 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -41,6 +41,12 @@ import { getRateLimiterPoints } from "./rate-limiter"; import { cleanOldConcurrencyLimitEntries, pushConcurrencyLimitActiveJob, removeConcurrencyLimitActiveJob, takeConcurrencyLimitedJob } from "../lib/concurrency-limit"; configDotenv(); +class RacedRedirectError extends Error { + constructor() { + super("Raced redirect error") + } +} + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const workerLockDuration = Number(process.env.WORKER_LOCK_DURATION) || 60000; @@ -434,8 +440,17 @@ async function processJob(job: Job & { id: string }, token: string) { // Remove the old URL from visited unique due to checking for limit // Do not remove from :visited otherwise it will keep crawling the original URL (sourceURL) await redisConnection.srem("crawl:" + job.data.crawl_id + ":visited_unique", normalizeURL(doc.metadata.sourceURL, sc)); + + const p1 = generateURLPermutations(normalizeURL(doc.metadata.url, sc)); + const p2 = generateURLPermutations(normalizeURL(doc.metadata.sourceURL, sc)); + + // In crawls, we should only crawl a redirected page once, no matter how many; times it is redirected to, or if it's been discovered by the crawler before. + // This can prevent flakiness with race conditions. // Lock the new URL - await lockURL(job.data.crawl_id, sc, doc.metadata.url); + const lockRes = await lockURL(job.data.crawl_id, sc, doc.metadata.url); + if (job.data.crawlerOptions !== null && !lockRes && JSON.stringify(p1) !== JSON.stringify(p2)) { + throw new RacedRedirectError(); + } } logger.debug("Logging job to DB..."); @@ -455,7 +470,7 @@ async function processJob(job: Job & { id: string }, token: string) { }, true); logger.debug("Declaring job as done..."); - await addCrawlJobDone(job.data.crawl_id, job.id); + await addCrawlJobDone(job.data.crawl_id, job.id, true); if (job.data.crawlerOptions !== null) { if (!sc.cancelled) { @@ -520,7 +535,11 @@ async function processJob(job: Job & { id: string }, token: string) { } catch (error) { const isEarlyTimeout = error instanceof Error && error.message === "timeout"; - if (!isEarlyTimeout) { + if (isEarlyTimeout) { + logger.error(`🐂 Job timed out ${job.id}`); + } else if (error instanceof RacedRedirectError) { + logger.warn(`🐂 Job got redirect raced ${job.id}, silently failing`); + } else { logger.error(`🐂 Job errored ${job.id} - ${error}`, { error }); Sentry.captureException(error, { @@ -537,8 +556,6 @@ async function processJob(job: Job & { id: string }, token: string) { if (error.stack) { logger.error(error.stack); } - } else { - logger.error(`🐂 Job timed out ${job.id}`); } const data = { @@ -573,7 +590,7 @@ async function processJob(job: Job & { id: string }, token: string) { const sc = (await getCrawl(job.data.crawl_id)) as StoredCrawl; logger.debug("Declaring job as done..."); - await addCrawlJobDone(job.data.crawl_id, job.id); + await addCrawlJobDone(job.data.crawl_id, job.id, false); logger.debug("Logging job to DB..."); await logJob({ From d276a23da0f553c7b27653d0f9fbed8546b4fccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Tue, 10 Dec 2024 23:24:33 +0100 Subject: [PATCH 18/52] fix(scrapeURL/pdf): handle if a presumed PDF link returns HTML (e.g. 404) --- .../scraper/scrapeURL/engines/pdf/index.ts | 70 +++++++++++++------ apps/api/src/scraper/scrapeURL/error.ts | 9 +++ apps/api/src/scraper/scrapeURL/index.ts | 7 +- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts index 19f0a7a8..b441943c 100644 --- a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts @@ -8,6 +8,7 @@ import * as Sentry from "@sentry/node"; import escapeHtml from "escape-html"; import PdfParse from "pdf-parse"; import { downloadFile, fetchFileToBuffer } from "../utils/downloadFile"; +import { RemoveFeatureError } from "../../error"; type PDFProcessorResult = {html: string, markdown?: string}; @@ -52,24 +53,47 @@ async function scrapePDFWithLlamaParse(meta: Meta, tempFilePath: string): Promis const jobId = upload.id; // TODO: timeout, retries - const result = await robustFetch({ - url: `https://api.cloud.llamaindex.ai/api/parsing/job/${jobId}/result/markdown`, - method: "GET", - headers: { - "Authorization": `Bearer ${process.env.LLAMAPARSE_API_KEY}`, - }, - logger: meta.logger.child({ method: "scrapePDFWithLlamaParse/result/robustFetch" }), - schema: z.object({ - markdown: z.string(), - }), - tryCount: meta.options.timeout !== undefined ? 32 : 1200, // 5 minutes if timeout not specified - tryCooldown: 250, - }); - - return { - markdown: result.markdown, - html: await marked.parse(result.markdown, { async: true }), - }; + const startedAt = Date.now(); + + while (Date.now() <= startedAt + (meta.options.timeout ?? 300000)) { + try { + const result = await robustFetch({ + url: `https://api.cloud.llamaindex.ai/api/parsing/job/${jobId}/result/markdown`, + method: "GET", + headers: { + "Authorization": `Bearer ${process.env.LLAMAPARSE_API_KEY}`, + }, + logger: meta.logger.child({ method: "scrapePDFWithLlamaParse/result/robustFetch" }), + schema: z.object({ + markdown: z.string(), + }), + }); + return { + markdown: result.markdown, + html: await marked.parse(result.markdown, { async: true }), + }; + } catch (e) { + if (e instanceof Error && e.message === "Request sent failure status") { + if ((e.cause as any).response.status === 404) { + // no-op, result not up yet + } else if ((e.cause as any).response.body.includes("PDF_IS_BROKEN")) { + // URL is not a PDF, actually! + meta.logger.debug("URL is not actually a PDF, signalling..."); + throw new RemoveFeatureError(["pdf"]); + } else { + throw new Error("LlamaParse threw an error", { + cause: e.cause, + }); + } + } else { + throw e; + } + } + + await new Promise((resolve) => setTimeout(() => resolve(), 250)); + } + + throw new Error("LlamaParse timed out"); } async function scrapePDFWithParsePDF(meta: Meta, tempFilePath: string): Promise { @@ -107,8 +131,14 @@ export async function scrapePDF(meta: Meta): Promise { logger: meta.logger.child({ method: "scrapePDF/scrapePDFWithLlamaParse" }), }, tempFilePath); } catch (error) { - meta.logger.warn("LlamaParse failed to parse PDF -- falling back to parse-pdf", { error }); - Sentry.captureException(error); + if (error instanceof Error && error.message === "LlamaParse timed out") { + meta.logger.warn("LlamaParse timed out -- falling back to parse-pdf", { error }); + } else if (error instanceof RemoveFeatureError) { + throw error; + } else { + meta.logger.warn("LlamaParse failed to parse PDF -- falling back to parse-pdf", { error }); + Sentry.captureException(error); + } } } diff --git a/apps/api/src/scraper/scrapeURL/error.ts b/apps/api/src/scraper/scrapeURL/error.ts index 3eb46033..ccd7a359 100644 --- a/apps/api/src/scraper/scrapeURL/error.ts +++ b/apps/api/src/scraper/scrapeURL/error.ts @@ -33,6 +33,15 @@ export class AddFeatureError extends Error { } } +export class RemoveFeatureError extends Error { + public featureFlags: FeatureFlag[]; + + constructor(featureFlags: FeatureFlag[]) { + super("Incorrect feature flags have been discovered: " + featureFlags.join(", ")); + this.featureFlags = featureFlags; + } +} + export class SiteError extends Error { public code: string; constructor(code: string) { diff --git a/apps/api/src/scraper/scrapeURL/index.ts b/apps/api/src/scraper/scrapeURL/index.ts index ad006d5a..f394ca2b 100644 --- a/apps/api/src/scraper/scrapeURL/index.ts +++ b/apps/api/src/scraper/scrapeURL/index.ts @@ -5,7 +5,7 @@ import { Document, ScrapeOptions } from "../../controllers/v1/types"; import { logger } from "../../lib/logger"; import { buildFallbackList, Engine, EngineScrapeResult, FeatureFlag, scrapeURLWithEngine } from "./engines"; import { parseMarkdown } from "../../lib/html-to-markdown"; -import { AddFeatureError, EngineError, NoEnginesLeftError, SiteError, TimeoutError } from "./error"; +import { AddFeatureError, EngineError, NoEnginesLeftError, RemoveFeatureError, SiteError, TimeoutError } from "./error"; import { executeTransformers } from "./transformers"; import { LLMRefusalError } from "./transformers/llmExtract"; import { urlSpecificParams } from "./lib/urlSpecificParams"; @@ -216,7 +216,7 @@ async function scrapeURLLoop( startedAt, finishedAt: Date.now(), }; - } else if (error instanceof AddFeatureError) { + } else if (error instanceof AddFeatureError || error instanceof RemoveFeatureError) { throw error; } else if (error instanceof LLMRefusalError) { results[engine] = { @@ -293,6 +293,9 @@ export async function scrapeURL( if (error instanceof AddFeatureError && meta.internalOptions.forceEngine === undefined) { meta.logger.debug("More feature flags requested by scraper: adding " + error.featureFlags.join(", "), { error, existingFlags: meta.featureFlags }); meta.featureFlags = new Set([...meta.featureFlags].concat(error.featureFlags)); + } else if (error instanceof RemoveFeatureError && meta.internalOptions.forceEngine === undefined) { + meta.logger.debug("Incorrect feature flags reported by scraper: removing " + error.featureFlags.join(","), { error, existingFlags: meta.featureFlags }); + meta.featureFlags = new Set([...meta.featureFlags].filter(x => !error.featureFlags.includes(x))); } else { throw error; } From f877fbfb8f0a8f4426c20fa900734bba196fd50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Tue, 10 Dec 2024 23:24:53 +0100 Subject: [PATCH 19/52] fix(WebCrawler/isFile): add .wav --- apps/api/src/scraper/WebScraper/crawler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/scraper/WebScraper/crawler.ts b/apps/api/src/scraper/WebScraper/crawler.ts index 87b2c437..cac03a68 100644 --- a/apps/api/src/scraper/WebScraper/crawler.ts +++ b/apps/api/src/scraper/WebScraper/crawler.ts @@ -335,6 +335,7 @@ export class WebCrawler { ".dmg", ".mp4", ".mp3", + ".wav", ".pptx", // ".docx", ".xlsx", From e5fe9e1534cc80dc94b811604081bbe07606af38 Mon Sep 17 00:00:00 2001 From: Eric Ciarla Date: Wed, 11 Dec 2024 15:31:08 -0500 Subject: [PATCH 20/52] Create .env.example --- examples/automated_price_tracking/.env.example | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 examples/automated_price_tracking/.env.example diff --git a/examples/automated_price_tracking/.env.example b/examples/automated_price_tracking/.env.example new file mode 100644 index 00000000..4a9dbf9a --- /dev/null +++ b/examples/automated_price_tracking/.env.example @@ -0,0 +1,2 @@ +FIRECRAWL_API_KEY= +POSTGRES_URL= \ No newline at end of file From 00335e2ba9a827db9964a9b82c5feaa990633533 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 11 Dec 2024 19:46:11 -0300 Subject: [PATCH 21/52] Nick: fixed prettier --- apps/api/.prettierrc | 3 + apps/api/package.json | 2 +- .../src/__tests__/e2e_extract/index.test.ts | 509 ++-- .../__tests__/e2e_full_withAuth/index.test.ts | 2246 +++++++++-------- apps/api/src/__tests__/e2e_map/index.test.ts | 10 +- .../src/__tests__/e2e_noAuth/index.test.ts | 15 +- .../__tests__/e2e_v1_withAuth/index.test.ts | 1487 +++++------ .../e2e_v1_withAuth_all_params/index.test.ts | 576 +++-- .../src/__tests__/e2e_withAuth/index.test.ts | 51 +- .../src/controllers/__tests__/crawl.test.ts | 38 +- apps/api/src/controllers/auth.ts | 39 +- apps/api/src/controllers/v0/admin/queue.ts | 18 +- .../src/controllers/v0/admin/redis-health.ts | 2 +- apps/api/src/controllers/v0/crawl-cancel.ts | 8 +- apps/api/src/controllers/v0/crawl-status.ts | 66 +- apps/api/src/controllers/v0/crawl.ts | 75 +- apps/api/src/controllers/v0/crawlPreview.ts | 92 +- apps/api/src/controllers/v0/keyAuth.ts | 8 +- apps/api/src/controllers/v0/scrape.ts | 87 +- apps/api/src/controllers/v0/search.ts | 77 +- apps/api/src/controllers/v0/status.ts | 29 +- .../v1/__tests__/urlValidation.test.ts | 32 +- apps/api/src/controllers/v1/batch-scrape.ts | 76 +- .../src/controllers/v1/concurrency-check.ts | 2 +- apps/api/src/controllers/v1/crawl-cancel.ts | 7 +- .../api/src/controllers/v1/crawl-status-ws.ts | 113 +- apps/api/src/controllers/v1/crawl-status.ts | 122 +- apps/api/src/controllers/v1/crawl.ts | 91 +- apps/api/src/controllers/v1/extract.ts | 124 +- apps/api/src/controllers/v1/map.ts | 50 +- apps/api/src/controllers/v1/scrape-status.ts | 12 +- apps/api/src/controllers/v1/scrape.ts | 44 +- apps/api/src/controllers/v1/types.ts | 566 +++-- apps/api/src/index.ts | 125 +- apps/api/src/lib/LLM-extraction/index.ts | 2 +- apps/api/src/lib/LLM-extraction/models.ts | 26 +- .../lib/__tests__/html-to-markdown.test.ts | 46 +- .../src/lib/__tests__/job-priority.test.ts | 6 +- apps/api/src/lib/batch-process.ts | 27 +- apps/api/src/lib/cache.ts | 74 +- apps/api/src/lib/concurrency-limit.ts | 81 +- apps/api/src/lib/crawl-redis.test.ts | 62 +- apps/api/src/lib/crawl-redis.ts | 401 +-- apps/api/src/lib/custom-error.ts | 3 +- apps/api/src/lib/default-values.ts | 6 +- apps/api/src/lib/entities.ts | 86 +- apps/api/src/lib/extract/build-document.ts | 8 +- apps/api/src/lib/extract/reranker.ts | 12 +- apps/api/src/lib/html-to-markdown.ts | 50 +- apps/api/src/lib/job-priority.ts | 2 +- apps/api/src/lib/logger.ts | 58 +- apps/api/src/lib/parseApi.ts | 1 - apps/api/src/lib/ranker.test.ts | 57 +- apps/api/src/lib/ranker.ts | 66 +- apps/api/src/lib/scrape-events.ts | 96 +- apps/api/src/lib/supabase-jobs.ts | 5 +- apps/api/src/lib/timeout.ts | 2 +- apps/api/src/lib/validate-country.ts | 502 ++-- apps/api/src/lib/validateUrl.test.ts | 68 +- apps/api/src/lib/validateUrl.ts | 65 +- apps/api/src/lib/withAuth.ts | 4 +- apps/api/src/main/runWebScraper.ts | 114 +- apps/api/src/routes/admin.ts | 9 +- apps/api/src/routes/v0.ts | 2 +- apps/api/src/routes/v1.ts | 270 +- apps/api/src/run-req.ts | 18 +- .../WebScraper/__tests__/crawler.test.ts | 67 +- .../scraper/WebScraper/__tests__/dns.test.ts | 4 +- apps/api/src/scraper/WebScraper/crawler.ts | 190 +- .../WebScraper/custom/handleCustomScraping.ts | 27 +- apps/api/src/scraper/WebScraper/sitemap.ts | 72 +- .../utils/__tests__/blocklist.test.ts | 138 +- .../utils/__tests__/maxDepthUtils.test.ts | 47 +- .../src/scraper/WebScraper/utils/blocklist.ts | 95 +- .../scraper/WebScraper/utils/maxDepthUtils.ts | 11 +- .../WebScraper/utils/removeBase64Images.ts | 6 +- .../scraper/scrapeURL/engines/cache/index.ts | 22 +- .../scraper/scrapeURL/engines/docx/index.ts | 12 +- .../scraper/scrapeURL/engines/fetch/index.ts | 46 +- .../engines/fire-engine/checkStatus.ts | 181 +- .../scrapeURL/engines/fire-engine/delete.ts | 53 +- .../scrapeURL/engines/fire-engine/index.ts | 426 ++-- .../scrapeURL/engines/fire-engine/scrape.ts | 136 +- .../src/scraper/scrapeURL/engines/index.ts | 583 +++-- .../scraper/scrapeURL/engines/pdf/index.ts | 283 ++- .../scrapeURL/engines/playwright/index.ts | 71 +- .../scrapeURL/engines/scrapingbee/index.ts | 116 +- .../scrapeURL/engines/utils/downloadFile.ts | 79 +- .../engines/utils/specialtyHandler.ts | 36 +- apps/api/src/scraper/scrapeURL/error.ts | 66 +- apps/api/src/scraper/scrapeURL/index.ts | 614 +++-- .../src/scraper/scrapeURL/lib/extractLinks.ts | 61 +- .../scraper/scrapeURL/lib/extractMetadata.ts | 57 +- apps/api/src/scraper/scrapeURL/lib/fetch.ts | 317 ++- .../scrapeURL/lib/removeUnwantedElements.ts | 181 +- .../scrapeURL/lib/urlSpecificParams.ts | 76 +- .../src/scraper/scrapeURL/scrapeURL.test.ts | 847 ++++--- .../scraper/scrapeURL/transformers/cache.ts | 46 +- .../scraper/scrapeURL/transformers/index.ts | 235 +- .../scrapeURL/transformers/llmExtract.ts | 366 +-- .../transformers/removeBase64Images.ts | 13 +- .../transformers/uploadScreenshot.ts | 36 +- apps/api/src/search/fireEngine.ts | 6 +- apps/api/src/search/googlesearch.ts | 238 +- apps/api/src/search/index.ts | 4 +- apps/api/src/search/searchapi.ts | 16 +- apps/api/src/search/serper.ts | 17 +- apps/api/src/services/alerts/index.ts | 2 +- apps/api/src/services/alerts/slack.ts | 6 +- apps/api/src/services/billing/auto_charge.ts | 246 +- .../src/services/billing/credit_billing.ts | 82 +- .../api/src/services/billing/issue_credits.ts | 2 +- apps/api/src/services/billing/stripe.ts | 11 +- apps/api/src/services/idempotency/create.ts | 6 +- apps/api/src/services/idempotency/validate.ts | 24 +- apps/api/src/services/logging/crawl_log.ts | 16 +- apps/api/src/services/logging/log_job.ts | 42 +- apps/api/src/services/logging/scrape_log.ts | 6 +- .../notification/email_notification.ts | 188 +- apps/api/src/services/posthog.ts | 6 +- apps/api/src/services/queue-jobs.ts | 95 +- apps/api/src/services/queue-service.ts | 13 +- apps/api/src/services/queue-worker.ts | 359 ++- apps/api/src/services/rate-limiter.test.ts | 8 +- apps/api/src/services/rate-limiter.ts | 66 +- apps/api/src/services/redis.ts | 7 +- apps/api/src/services/redlock.ts | 2 +- apps/api/src/services/sentry.ts | 6 +- apps/api/src/services/supabase.ts | 4 +- apps/api/src/services/system-monitor.ts | 401 +-- apps/api/src/services/webhook.ts | 43 +- apps/api/src/strings.ts | 2 +- apps/api/src/supabase_types.ts | 30 +- apps/api/src/types.ts | 72 +- 134 files changed, 9565 insertions(+), 7108 deletions(-) create mode 100644 apps/api/.prettierrc diff --git a/apps/api/.prettierrc b/apps/api/.prettierrc new file mode 100644 index 00000000..d93a7f24 --- /dev/null +++ b/apps/api/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "none" +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 56724de7..86f798e9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "nodemon --exec ts-node src/index.ts", "start:production": "tsc && node dist/src/index.js", - "format": "prettier --write \"src/**/*.(js|ts)\"", + "format": "npx prettier --write \"src/**/*.(js|ts)\"", "flyio": "node dist/src/index.js", "start:dev": "nodemon --exec ts-node src/index.ts", "build": "tsc && pnpm sentry:sourcemaps", diff --git a/apps/api/src/__tests__/e2e_extract/index.test.ts b/apps/api/src/__tests__/e2e_extract/index.test.ts index 679dc3cd..117cbab1 100644 --- a/apps/api/src/__tests__/e2e_extract/index.test.ts +++ b/apps/api/src/__tests__/e2e_extract/index.test.ts @@ -3,264 +3,305 @@ import dotenv from "dotenv"; import { FirecrawlCrawlResponse, FirecrawlCrawlStatusResponse, - FirecrawlScrapeResponse, + FirecrawlScrapeResponse } from "../../types"; dotenv.config(); const TEST_URL = "http://127.0.0.1:3002"; describe("E2E Tests for Extract API Routes", () => { - it.concurrent("should return authors of blog posts on firecrawl.dev", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["https://firecrawl.dev/*"], - prompt: "Who are the authors of the blog posts?", - schema: { - type: "object", - properties: { authors: { type: "array", items: { type: "string" } } }, - }, - }); - - console.log(response.body); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("authors"); - - let gotItRight = 0; - for (const author of response.body.data?.authors) { - if (author.includes("Caleb Peffer")) gotItRight++; - if (author.includes("Gergő Móricz")) gotItRight++; - if (author.includes("Eric Ciarla")) gotItRight++; - if (author.includes("Nicolas Camara")) gotItRight++; - if (author.includes("Jon")) gotItRight++; - if (author.includes("Wendong")) gotItRight++; - - } - - expect(gotItRight).toBeGreaterThan(1); - }, 60000); - - it.concurrent("should return founders of firecrawl.dev (allowExternalLinks = true)", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["firecrawl.dev/*"], - prompt: "Who are the founders of the company?", - allowExternalLinks: true, - schema: { - type: "object", - properties: { founders: { type: "array", items: { type: "string" } } }, - }, - }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("founders"); - - console.log(response.body.data?.founders); - let gotItRight = 0; - for (const founder of response.body.data?.founders) { - if (founder.includes("Caleb")) gotItRight++; - if (founder.includes("Eric")) gotItRight++; - if (founder.includes("Nicolas")) gotItRight++; - if (founder.includes("nick")) gotItRight++; - if (founder.includes("eric")) gotItRight++; - if (founder.includes("jon-noronha")) gotItRight++; - - } - - expect(gotItRight).toBeGreaterThanOrEqual(2); - }, 60000); - - it.concurrent("should return hiring opportunities on firecrawl.dev (allowExternalLinks = true)", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["https://firecrawl.dev/*"], - prompt: "What are they hiring for?", - allowExternalLinks: true, - schema: { - type: "array", - items: { - type: "string" - }, - required: ["items"] - }, - }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - console.log(response.body.data); - - let gotItRight = 0; - for (const hiring of response.body.data?.items) { - if (hiring.includes("Developer Support Engineer")) gotItRight++; - if (hiring.includes("Dev Ops Engineer")) gotItRight++; - if (hiring.includes("Founding Web Automation Engineer")) gotItRight++; - } - - expect(gotItRight).toBeGreaterThan(2); - }, 60000); - - it.concurrent("should return PCI DSS compliance for Fivetran", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["fivetran.com/*"], - prompt: "Does Fivetran have PCI DSS compliance?", - allowExternalLinks: true, - schema: { - type: "object", - properties: { - pciDssCompliance: { type: "boolean" } - } - }, - }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data?.pciDssCompliance).toBe(true); - }, 60000); - - it.concurrent("should return Azure Data Connectors for Fivetran", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["fivetran.com/*"], - prompt: "What are the Azure Data Connectors they offer?", - schema: { - type: "array", - items: { + it.concurrent( + "should return authors of blog posts on firecrawl.dev", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["https://firecrawl.dev/*"], + prompt: "Who are the authors of the blog posts?", + schema: { type: "object", properties: { - connector: { type: "string" }, - description: { type: "string" }, - supportsCaptureDelete: { type: "boolean" } + authors: { type: "array", items: { type: "string" } } } } - } - }) + }); - console.log(response.body); - // expect(response.statusCode).toBe(200); - // expect(response.body).toHaveProperty("data"); - // expect(response.body.data?.pciDssCompliance).toBe(true); - }, 60000); + console.log(response.body); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("authors"); - it.concurrent("should return Greenhouse Applicant Tracking System for Abnormal Security", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["https://careers.abnormalsecurity.com/jobs/6119456003?gh_jid=6119456003"], - prompt: "what applicant tracking system is this company using?", - schema: { - type: "object", - properties: { - isGreenhouseATS: { type: "boolean" }, - answer: { type: "string" } - } - }, - allowExternalLinks: true - }) + let gotItRight = 0; + for (const author of response.body.data?.authors) { + if (author.includes("Caleb Peffer")) gotItRight++; + if (author.includes("Gergő Móricz")) gotItRight++; + if (author.includes("Eric Ciarla")) gotItRight++; + if (author.includes("Nicolas Camara")) gotItRight++; + if (author.includes("Jon")) gotItRight++; + if (author.includes("Wendong")) gotItRight++; + } - console.log(response.body); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data?.isGreenhouseATS).toBe(true); - }, 60000); + expect(gotItRight).toBeGreaterThan(1); + }, + 60000 + ); - it.concurrent("should return mintlify api components", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["https://mintlify.com/docs/*"], - prompt: "what are the 4 API components?", - schema: { - type: "array", - items: { + it.concurrent( + "should return founders of firecrawl.dev (allowExternalLinks = true)", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["firecrawl.dev/*"], + prompt: "Who are the founders of the company?", + allowExternalLinks: true, + schema: { type: "object", properties: { - component: { type: "string" } + founders: { type: "array", items: { type: "string" } } + } + } + }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("founders"); + + console.log(response.body.data?.founders); + let gotItRight = 0; + for (const founder of response.body.data?.founders) { + if (founder.includes("Caleb")) gotItRight++; + if (founder.includes("Eric")) gotItRight++; + if (founder.includes("Nicolas")) gotItRight++; + if (founder.includes("nick")) gotItRight++; + if (founder.includes("eric")) gotItRight++; + if (founder.includes("jon-noronha")) gotItRight++; + } + + expect(gotItRight).toBeGreaterThanOrEqual(2); + }, + 60000 + ); + + it.concurrent( + "should return hiring opportunities on firecrawl.dev (allowExternalLinks = true)", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["https://firecrawl.dev/*"], + prompt: "What are they hiring for?", + allowExternalLinks: true, + schema: { + type: "array", + items: { + type: "string" + }, + required: ["items"] + } + }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + console.log(response.body.data); + + let gotItRight = 0; + for (const hiring of response.body.data?.items) { + if (hiring.includes("Developer Support Engineer")) gotItRight++; + if (hiring.includes("Dev Ops Engineer")) gotItRight++; + if (hiring.includes("Founding Web Automation Engineer")) gotItRight++; + } + + expect(gotItRight).toBeGreaterThan(2); + }, + 60000 + ); + + it.concurrent( + "should return PCI DSS compliance for Fivetran", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["fivetran.com/*"], + prompt: "Does Fivetran have PCI DSS compliance?", + allowExternalLinks: true, + schema: { + type: "object", + properties: { + pciDssCompliance: { type: "boolean" } + } + } + }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data?.pciDssCompliance).toBe(true); + }, + 60000 + ); + + it.concurrent( + "should return Azure Data Connectors for Fivetran", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["fivetran.com/*"], + prompt: "What are the Azure Data Connectors they offer?", + schema: { + type: "array", + items: { + type: "object", + properties: { + connector: { type: "string" }, + description: { type: "string" }, + supportsCaptureDelete: { type: "boolean" } + } + } + } + }); + + console.log(response.body); + // expect(response.statusCode).toBe(200); + // expect(response.body).toHaveProperty("data"); + // expect(response.body.data?.pciDssCompliance).toBe(true); + }, + 60000 + ); + + it.concurrent( + "should return Greenhouse Applicant Tracking System for Abnormal Security", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: [ + "https://careers.abnormalsecurity.com/jobs/6119456003?gh_jid=6119456003" + ], + prompt: "what applicant tracking system is this company using?", + schema: { + type: "object", + properties: { + isGreenhouseATS: { type: "boolean" }, + answer: { type: "string" } } }, - required: ["items"] - }, - allowExternalLinks: true - }) + allowExternalLinks: true + }); - console.log(response.body.data?.items); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data?.items.length).toBe(4); - let gotItRight = 0; - for (const component of response.body.data?.items) { - if (component.component.toLowerCase().includes("parameter")) gotItRight++; - if (component.component.toLowerCase().includes("response")) gotItRight++; - if (component.component.toLowerCase().includes("expandable")) gotItRight++; - if (component.component.toLowerCase().includes("sticky")) gotItRight++; - if (component.component.toLowerCase().includes("examples")) gotItRight++; + console.log(response.body); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data?.isGreenhouseATS).toBe(true); + }, + 60000 + ); - } - expect(gotItRight).toBeGreaterThan(2); - }, 60000); - - it.concurrent("should return information about Eric Ciarla", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["https://ericciarla.com/"], - prompt: "Who is Eric Ciarla? Where does he work? Where did he go to school?", - schema: { - type: "object", - properties: { - name: { type: "string" }, - work: { type: "string" }, - education: { type: "string" } + it.concurrent( + "should return mintlify api components", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["https://mintlify.com/docs/*"], + prompt: "what are the 4 API components?", + schema: { + type: "array", + items: { + type: "object", + properties: { + component: { type: "string" } + } + }, + required: ["items"] }, - required: ["name", "work", "education"] - }, - allowExternalLinks: true - }) + allowExternalLinks: true + }); - console.log(response.body.data); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data?.name).toBe("Eric Ciarla"); - expect(response.body.data?.work).toBeDefined(); - expect(response.body.data?.education).toBeDefined(); - }, 60000); + console.log(response.body.data?.items); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data?.items.length).toBe(4); + let gotItRight = 0; + for (const component of response.body.data?.items) { + if (component.component.toLowerCase().includes("parameter")) + gotItRight++; + if (component.component.toLowerCase().includes("response")) + gotItRight++; + if (component.component.toLowerCase().includes("expandable")) + gotItRight++; + if (component.component.toLowerCase().includes("sticky")) gotItRight++; + if (component.component.toLowerCase().includes("examples")) + gotItRight++; + } + expect(gotItRight).toBeGreaterThan(2); + }, + 60000 + ); - it.concurrent("should extract information without a schema", async () => { - const response = await request(TEST_URL) - .post("/v1/extract") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - urls: ["https://docs.firecrawl.dev"], - prompt: "What is the title and description of the page?" - }); + it.concurrent( + "should return information about Eric Ciarla", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["https://ericciarla.com/"], + prompt: + "Who is Eric Ciarla? Where does he work? Where did he go to school?", + schema: { + type: "object", + properties: { + name: { type: "string" }, + work: { type: "string" }, + education: { type: "string" } + }, + required: ["name", "work", "education"] + }, + allowExternalLinks: true + }); - console.log(response.body.data); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(typeof response.body.data).toBe("object"); - expect(Object.keys(response.body.data).length).toBeGreaterThan(0); - }, 60000); + console.log(response.body.data); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data?.name).toBe("Eric Ciarla"); + expect(response.body.data?.work).toBeDefined(); + expect(response.body.data?.education).toBeDefined(); + }, + 60000 + ); - + it.concurrent( + "should extract information without a schema", + async () => { + const response = await request(TEST_URL) + .post("/v1/extract") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + urls: ["https://docs.firecrawl.dev"], + prompt: "What is the title and description of the page?" + }); + console.log(response.body.data); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(typeof response.body.data).toBe("object"); + expect(Object.keys(response.body.data).length).toBeGreaterThan(0); + }, + 60000 + ); }); diff --git a/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts b/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts index dec77131..a8841aab 100644 --- a/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts @@ -38,14 +38,17 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(401); }); - it.concurrent("should return an error response with an invalid API key", async () => { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer invalid-api-key`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(401); - }); + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer invalid-api-key`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + expect(response.statusCode).toBe(401); + } + ); it.concurrent("should return an error for a blocklisted URL", async () => { const blocklistedUrl = "https://facebook.com/fake-test"; @@ -70,172 +73,232 @@ describe("E2E Tests for API Routes", () => { // expect(response.statusCode).toBe(200); // }, 30000); // 30 seconds timeout - it.concurrent("should return a successful response with a valid API key", async () => { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://roastmywebsite.ai" }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("content"); - expect(response.body.data).toHaveProperty("markdown"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data).not.toHaveProperty("html"); - expect(response.body.data.content).toContain("_Roast_"); - expect(response.body.data.metadata).toHaveProperty("title"); - expect(response.body.data.metadata).toHaveProperty("description"); - expect(response.body.data.metadata).toHaveProperty("keywords"); - expect(response.body.data.metadata).toHaveProperty("robots"); - expect(response.body.data.metadata).toHaveProperty("ogTitle"); - expect(response.body.data.metadata).toHaveProperty("ogDescription"); - expect(response.body.data.metadata).toHaveProperty("ogUrl"); - expect(response.body.data.metadata).toHaveProperty("ogImage"); - expect(response.body.data.metadata).toHaveProperty("ogLocaleAlternate"); - expect(response.body.data.metadata).toHaveProperty("ogSiteName"); - expect(response.body.data.metadata).toHaveProperty("sourceURL"); - expect(response.body.data.metadata).toHaveProperty("pageStatusCode"); - expect(response.body.data.metadata.pageError).toBeUndefined(); - expect(response.body.data.metadata.title).toBe("Roast My Website"); - expect(response.body.data.metadata.description).toBe("Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️"); - expect(response.body.data.metadata.keywords).toBe("Roast My Website,Roast,Website,GitHub,Firecrawl"); - expect(response.body.data.metadata.robots).toBe("follow, index"); - expect(response.body.data.metadata.ogTitle).toBe("Roast My Website"); - expect(response.body.data.metadata.ogDescription).toBe("Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️"); - expect(response.body.data.metadata.ogUrl).toBe("https://www.roastmywebsite.ai"); - expect(response.body.data.metadata.ogImage).toBe("https://www.roastmywebsite.ai/og.png"); - expect(response.body.data.metadata.ogLocaleAlternate).toStrictEqual([]); - expect(response.body.data.metadata.ogSiteName).toBe("Roast My Website"); - expect(response.body.data.metadata.sourceURL).toBe("https://roastmywebsite.ai"); - expect(response.body.data.metadata.pageStatusCode).toBe(200); - }, 30000); // 30 seconds timeout + it.concurrent( + "should return a successful response with a valid API key", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://roastmywebsite.ai" }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data).not.toHaveProperty("html"); + expect(response.body.data.content).toContain("_Roast_"); + expect(response.body.data.metadata).toHaveProperty("title"); + expect(response.body.data.metadata).toHaveProperty("description"); + expect(response.body.data.metadata).toHaveProperty("keywords"); + expect(response.body.data.metadata).toHaveProperty("robots"); + expect(response.body.data.metadata).toHaveProperty("ogTitle"); + expect(response.body.data.metadata).toHaveProperty("ogDescription"); + expect(response.body.data.metadata).toHaveProperty("ogUrl"); + expect(response.body.data.metadata).toHaveProperty("ogImage"); + expect(response.body.data.metadata).toHaveProperty("ogLocaleAlternate"); + expect(response.body.data.metadata).toHaveProperty("ogSiteName"); + expect(response.body.data.metadata).toHaveProperty("sourceURL"); + expect(response.body.data.metadata).toHaveProperty("pageStatusCode"); + expect(response.body.data.metadata.pageError).toBeUndefined(); + expect(response.body.data.metadata.title).toBe("Roast My Website"); + expect(response.body.data.metadata.description).toBe( + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + ); + expect(response.body.data.metadata.keywords).toBe( + "Roast My Website,Roast,Website,GitHub,Firecrawl" + ); + expect(response.body.data.metadata.robots).toBe("follow, index"); + expect(response.body.data.metadata.ogTitle).toBe("Roast My Website"); + expect(response.body.data.metadata.ogDescription).toBe( + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + ); + expect(response.body.data.metadata.ogUrl).toBe( + "https://www.roastmywebsite.ai" + ); + expect(response.body.data.metadata.ogImage).toBe( + "https://www.roastmywebsite.ai/og.png" + ); + expect(response.body.data.metadata.ogLocaleAlternate).toStrictEqual([]); + expect(response.body.data.metadata.ogSiteName).toBe("Roast My Website"); + expect(response.body.data.metadata.sourceURL).toBe( + "https://roastmywebsite.ai" + ); + expect(response.body.data.metadata.pageStatusCode).toBe(200); + }, + 30000 + ); // 30 seconds timeout - it.concurrent("should return a successful response with a valid API key and includeHtml set to true", async () => { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true }, - }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("content"); - expect(response.body.data).toHaveProperty("markdown"); - expect(response.body.data).toHaveProperty("html"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data.content).toContain("_Roast_"); - expect(response.body.data.markdown).toContain("_Roast_"); - expect(response.body.data.html).toContain(" { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://roastmywebsite.ai", + pageOptions: { includeHtml: true } + }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("html"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.content).toContain("_Roast_"); + expect(response.body.data.markdown).toContain("_Roast_"); + expect(response.body.data.html).toContain(" { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://roastmywebsite.ai", - pageOptions: { includeRawHtml: true }, - }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("content"); - expect(response.body.data).toHaveProperty("markdown"); - expect(response.body.data).toHaveProperty("rawHtml"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data.content).toContain("_Roast_"); - expect(response.body.data.markdown).toContain("_Roast_"); - expect(response.body.data.rawHtml).toContain(" { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://arxiv.org/pdf/astro-ph/9301001.pdf' }); - await new Promise((r) => setTimeout(r, 6000)); + it.concurrent( + "should return a successful response with a valid API key and includeRawHtml set to true", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://roastmywebsite.ai", + pageOptions: { includeRawHtml: true } + }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("rawHtml"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.content).toContain("_Roast_"); + expect(response.body.data.markdown).toContain("_Roast_"); + expect(response.body.data.rawHtml).toContain(" { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://arxiv.org/pdf/astro-ph/9301001' }); - await new Promise((r) => setTimeout(r, 6000)); + it.concurrent( + "should return a successful response for a valid scrape with PDF file", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://arxiv.org/pdf/astro-ph/9301001.pdf" }); + await new Promise((r) => setTimeout(r, 6000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.content).toContain('We present spectrophotometric observations of the Broad Line Radio Galaxy'); - expect(response.body.data.metadata.pageStatusCode).toBe(200); - expect(response.body.data.metadata.pageError).toBeUndefined(); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.content).toContain( + "We present spectrophotometric observations of the Broad Line Radio Galaxy" + ); + expect(response.body.data.metadata.pageStatusCode).toBe(200); + expect(response.body.data.metadata.pageError).toBeUndefined(); + }, + 60000 + ); // 60 seconds - it.concurrent('should return a successful response for a valid scrape with PDF file and parsePDF set to false', async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://arxiv.org/pdf/astro-ph/9301001.pdf', pageOptions: { parsePDF: false } }); - await new Promise((r) => setTimeout(r, 6000)); + it.concurrent( + "should return a successful response for a valid scrape with PDF file without explicit .pdf extension", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://arxiv.org/pdf/astro-ph/9301001" }); + await new Promise((r) => setTimeout(r, 6000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.content).toContain('/Title(arXiv:astro-ph/9301001v1 7 Jan 1993)>>endobj'); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.content).toContain( + "We present spectrophotometric observations of the Broad Line Radio Galaxy" + ); + expect(response.body.data.metadata.pageStatusCode).toBe(200); + expect(response.body.data.metadata.pageError).toBeUndefined(); + }, + 60000 + ); // 60 seconds - it.concurrent("should return a successful response with a valid API key with removeTags option", async () => { - const responseWithoutRemoveTags = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://www.scrapethissite.com/" }); - expect(responseWithoutRemoveTags.statusCode).toBe(200); - expect(responseWithoutRemoveTags.body).toHaveProperty("data"); - expect(responseWithoutRemoveTags.body.data).toHaveProperty("content"); - expect(responseWithoutRemoveTags.body.data).toHaveProperty("markdown"); - expect(responseWithoutRemoveTags.body.data).toHaveProperty("metadata"); - expect(responseWithoutRemoveTags.body.data).not.toHaveProperty("html"); - expect(responseWithoutRemoveTags.body.data.content).toContain("Scrape This Site"); - expect(responseWithoutRemoveTags.body.data.content).toContain("Lessons and Videos"); // #footer - expect(responseWithoutRemoveTags.body.data.content).toContain("[Sandbox]("); // .nav - expect(responseWithoutRemoveTags.body.data.content).toContain("web scraping"); // strong + it.concurrent( + "should return a successful response for a valid scrape with PDF file and parsePDF set to false", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://arxiv.org/pdf/astro-ph/9301001.pdf", + pageOptions: { parsePDF: false } + }); + await new Promise((r) => setTimeout(r, 6000)); - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://www.scrapethissite.com/", pageOptions: { removeTags: ['.nav', '#footer', 'strong'] } }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - expect(response.body.data).toHaveProperty("content"); - expect(response.body.data).toHaveProperty("markdown"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data).not.toHaveProperty("html"); - expect(response.body.data.content).toContain("Scrape This Site"); - expect(response.body.data.content).not.toContain("Lessons and Videos"); // #footer - expect(response.body.data.content).not.toContain("[Sandbox]("); // .nav - expect(response.body.data.content).not.toContain("web scraping"); // strong - }, 30000); // 30 seconds timeout + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.content).toContain( + "/Title(arXiv:astro-ph/9301001v1 7 Jan 1993)>>endobj" + ); + }, + 60000 + ); // 60 seconds + + it.concurrent( + "should return a successful response with a valid API key with removeTags option", + async () => { + const responseWithoutRemoveTags = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://www.scrapethissite.com/" }); + expect(responseWithoutRemoveTags.statusCode).toBe(200); + expect(responseWithoutRemoveTags.body).toHaveProperty("data"); + expect(responseWithoutRemoveTags.body.data).toHaveProperty("content"); + expect(responseWithoutRemoveTags.body.data).toHaveProperty("markdown"); + expect(responseWithoutRemoveTags.body.data).toHaveProperty("metadata"); + expect(responseWithoutRemoveTags.body.data).not.toHaveProperty("html"); + expect(responseWithoutRemoveTags.body.data.content).toContain( + "Scrape This Site" + ); + expect(responseWithoutRemoveTags.body.data.content).toContain( + "Lessons and Videos" + ); // #footer + expect(responseWithoutRemoveTags.body.data.content).toContain( + "[Sandbox](" + ); // .nav + expect(responseWithoutRemoveTags.body.data.content).toContain( + "web scraping" + ); // strong + + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://www.scrapethissite.com/", + pageOptions: { removeTags: [".nav", "#footer", "strong"] } + }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data).not.toHaveProperty("html"); + expect(response.body.data.content).toContain("Scrape This Site"); + expect(response.body.data.content).not.toContain("Lessons and Videos"); // #footer + expect(response.body.data.content).not.toContain("[Sandbox]("); // .nav + expect(response.body.data.content).not.toContain("web scraping"); // strong + }, + 30000 + ); // 30 seconds timeout // TODO: add this test back once we nail the waitFor option to be more deterministic // it.concurrent("should return a successful response with a valid API key and waitFor option", async () => { @@ -258,101 +321,137 @@ describe("E2E Tests for API Routes", () => { // expect(duration).toBeGreaterThanOrEqual(7000); // }, 12000); // 12 seconds timeout - it.concurrent('should return a successful response for a scrape with 400 page', async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/400' }); - await new Promise((r) => setTimeout(r, 5000)); + it.concurrent( + "should return a successful response for a scrape with 400 page", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/400" }); + await new Promise((r) => setTimeout(r, 5000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.pageStatusCode).toBe(400); - expect(response.body.data.metadata.pageError.toLowerCase()).toContain("bad request"); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.pageStatusCode).toBe(400); + expect(response.body.data.metadata.pageError.toLowerCase()).toContain( + "bad request" + ); + }, + 60000 + ); // 60 seconds - it.concurrent('should return a successful response for a scrape with 401 page', async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/401' }); - await new Promise((r) => setTimeout(r, 5000)); + it.concurrent( + "should return a successful response for a scrape with 401 page", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/401" }); + await new Promise((r) => setTimeout(r, 5000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.pageStatusCode).toBe(401); - expect(response.body.data.metadata.pageError.toLowerCase()).toContain("unauthorized"); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.pageStatusCode).toBe(401); + expect(response.body.data.metadata.pageError.toLowerCase()).toContain( + "unauthorized" + ); + }, + 60000 + ); // 60 seconds - it.concurrent("should return a successful response for a scrape with 403 page", async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/403' }); + it.concurrent( + "should return a successful response for a scrape with 403 page", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/403" }); - await new Promise((r) => setTimeout(r, 5000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.pageStatusCode).toBe(403); - expect(response.body.data.metadata.pageError.toLowerCase()).toContain("forbidden"); - }, 60000); // 60 seconds + await new Promise((r) => setTimeout(r, 5000)); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.pageStatusCode).toBe(403); + expect(response.body.data.metadata.pageError.toLowerCase()).toContain( + "forbidden" + ); + }, + 60000 + ); // 60 seconds - it.concurrent('should return a successful response for a scrape with 404 page', async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/404' }); - await new Promise((r) => setTimeout(r, 5000)); + it.concurrent( + "should return a successful response for a scrape with 404 page", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/404" }); + await new Promise((r) => setTimeout(r, 5000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.pageStatusCode).toBe(404); - expect(response.body.data.metadata.pageError.toLowerCase()).toContain("not found"); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.pageStatusCode).toBe(404); + expect(response.body.data.metadata.pageError.toLowerCase()).toContain( + "not found" + ); + }, + 60000 + ); // 60 seconds - it.concurrent('should return a successful response for a scrape with 405 page', async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/405' }); - await new Promise((r) => setTimeout(r, 5000)); + it.concurrent( + "should return a successful response for a scrape with 405 page", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/405" }); + await new Promise((r) => setTimeout(r, 5000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.pageStatusCode).toBe(405); - expect(response.body.data.metadata.pageError.toLowerCase()).toContain("method not allowed"); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.pageStatusCode).toBe(405); + expect(response.body.data.metadata.pageError.toLowerCase()).toContain( + "method not allowed" + ); + }, + 60000 + ); // 60 seconds - it.concurrent('should return a successful response for a scrape with 500 page', async () => { - const response = await request(TEST_URL) - .post('/v0/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/500' }); - await new Promise((r) => setTimeout(r, 5000)); + it.concurrent( + "should return a successful response for a scrape with 500 page", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/500" }); + await new Promise((r) => setTimeout(r, 5000)); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('content'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.pageStatusCode).toBe(500); - expect(response.body.data.metadata.pageError.toLowerCase()).toContain("internal server error"); - }, 60000); // 60 seconds + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + expect(response.body.data).toHaveProperty("content"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.pageStatusCode).toBe(500); + expect(response.body.data.metadata.pageError.toLowerCase()).toContain( + "internal server error" + ); + }, + 60000 + ); // 60 seconds }); describe("POST /v0/crawl", () => { @@ -361,14 +460,17 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(401); }); - it.concurrent("should return an error response with an invalid API key", async () => { - const response = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer invalid-api-key`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(401); - }); + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer invalid-api-key`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + expect(response.statusCode).toBe(401); + } + ); it.concurrent("should return an error for a blocklisted URL", async () => { const blocklistedUrl = "https://twitter.com/fake-test"; @@ -383,56 +485,64 @@ describe("E2E Tests for API Routes", () => { ); }); - it.concurrent("should return a successful response with a valid API key for crawl", async () => { - const response = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("jobId"); - expect(response.body.jobId).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ - ); - }); - it.concurrent('should prevent duplicate requests using the same idempotency key', async () => { - const uniqueIdempotencyKey = uuidv4(); - - // First request with the idempotency key - const firstResponse = await request(TEST_URL) - .post('/v0/crawl') - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .set("x-idempotency-key", uniqueIdempotencyKey) - .send({ url: 'https://docs.firecrawl.dev' }); - - expect(firstResponse.statusCode).toBe(200); - - // Second request with the same idempotency key - const secondResponse = await request(TEST_URL) - .post('/v0/crawl') - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .set("x-idempotency-key", uniqueIdempotencyKey) - .send({ url: 'https://docs.firecrawl.dev' }); - - expect(secondResponse.statusCode).toBe(409); - expect(secondResponse.body.error).toBe('Idempotency key already used'); - }); + it.concurrent( + "should return a successful response with a valid API key for crawl", + async () => { + const response = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("jobId"); + expect(response.body.jobId).toMatch( + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + ); + } + ); + it.concurrent( + "should prevent duplicate requests using the same idempotency key", + async () => { + const uniqueIdempotencyKey = uuidv4(); + + // First request with the idempotency key + const firstResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .set("x-idempotency-key", uniqueIdempotencyKey) + .send({ url: "https://docs.firecrawl.dev" }); + + expect(firstResponse.statusCode).toBe(200); + + // Second request with the same idempotency key + const secondResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .set("x-idempotency-key", uniqueIdempotencyKey) + .send({ url: "https://docs.firecrawl.dev" }); + + expect(secondResponse.statusCode).toBe(409); + expect(secondResponse.body.error).toBe("Idempotency key already used"); + } + ); + + it.concurrent( + "should return a successful response with a valid API key and valid includes option", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://mendable.ai", + limit: 10, + crawlerOptions: { + includes: ["blog/*"] + } + }); - it.concurrent("should return a successful response with a valid API key and valid includes option", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai", - limit: 10, - crawlerOptions: { - includes: ["blog/*"], - }, - }); - let response; let isFinished = false; @@ -453,278 +563,322 @@ describe("E2E Tests for API Routes", () => { const completedResponse = response; const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(5); - urls.forEach((url: string) => { - expect(url.startsWith("https://www.mendable.ai/blog/")).toBeTruthy(); - }); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].content).toContain("Mendable"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); - }, 60000); // 60 seconds - - it.concurrent("should return a successful response with a valid API key and valid excludes option", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai", - limit: 10, - crawlerOptions: { - excludes: ["blog/*"], - }, + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(5); + urls.forEach((url: string) => { + expect(url.startsWith("https://www.mendable.ai/blog/")).toBeTruthy(); }); - - let isFinished = false; - let response; - while (!isFinished) { - response = await request(TEST_URL) + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].content).toContain("Mendable"); + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); + expect( + completedResponse.body.data[0].metadata.pageError + ).toBeUndefined(); + }, + 60000 + ); // 60 seconds + + it.concurrent( + "should return a successful response with a valid API key and valid excludes option", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://mendable.ai", + limit: 10, + crawlerOptions: { + excludes: ["blog/*"] + } + }); + + let isFinished = false; + let response; + + while (!isFinished) { + response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + isFinished = response.body.status === "completed"; + + if (!isFinished) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } + } + + const completedResponse = response; + + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(5); + urls.forEach((url: string) => { + expect(url.startsWith("https://wwww.mendable.ai/blog/")).toBeFalsy(); + }); + }, + 90000 + ); // 90 seconds + + it.concurrent( + "should return a successful response with a valid API key and limit to 3", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://mendable.ai", + crawlerOptions: { limit: 3 } + }); + + let isFinished = false; + let response; + + while (!isFinished) { + response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + isFinished = response.body.status === "completed"; + + if (!isFinished) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } + } + + const completedResponse = response; + + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data.length).toBe(3); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].content).toContain("Mendable"); + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); + expect( + completedResponse.body.data[0].metadata.pageError + ).toBeUndefined(); + }, + 60000 + ); // 60 seconds + + it.concurrent( + "should return a successful response with max depth option for a valid crawl job", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://www.scrapethissite.com", + crawlerOptions: { maxDepth: 1 } + }); + expect(crawlResponse.statusCode).toBe(200); + + const response = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("status"); - isFinished = response.body.status === "completed"; - - if (!isFinished) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + expect(["active", "waiting"]).toContain(response.body.status); + // wait for 60 seconds + let isCompleted = false; + while (!isCompleted) { + const statusCheckResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(statusCheckResponse.statusCode).toBe(200); + isCompleted = statusCheckResponse.body.status === "completed"; + if (!isCompleted) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } } - } - - const completedResponse = response; - - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(5); - urls.forEach((url: string) => { - expect(url.startsWith("https://wwww.mendable.ai/blog/")).toBeFalsy(); - }); - }, 90000); // 90 seconds - - it.concurrent("should return a successful response with a valid API key and limit to 3", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai", - crawlerOptions: { limit: 3 }, - }); - - let isFinished = false; - let response; - - while (!isFinished) { - response = await request(TEST_URL) + const completedResponse = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); + expect( + completedResponse.body.data[0].metadata.pageError + ).toBeUndefined(); + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(1); + + // Check if all URLs have a maximum depth of 1 + urls.forEach((url: string) => { + const pathSplits = new URL(url).pathname.split("/"); + const depth = + pathSplits.length - + (pathSplits[0].length === 0 && + pathSplits[pathSplits.length - 1].length === 0 + ? 1 + : 0); + expect(depth).toBeLessThanOrEqual(2); + }); + }, + 180000 + ); + + it.concurrent( + "should return a successful response with relative max depth option for a valid crawl job", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://www.scrapethissite.com/pages/", + crawlerOptions: { maxDepth: 1 } + }); + expect(crawlResponse.statusCode).toBe(200); + + const response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("status"); - isFinished = response.body.status === "completed"; - - if (!isFinished) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + expect(["active", "waiting"]).toContain(response.body.status); + // wait for 60 seconds + let isCompleted = false; + while (!isCompleted) { + const statusCheckResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(statusCheckResponse.statusCode).toBe(200); + isCompleted = statusCheckResponse.body.status === "completed"; + if (!isCompleted) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } } - } - - const completedResponse = response; - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data.length).toBe(3); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].content).toContain("Mendable"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); - }, 60000); // 60 seconds - - it.concurrent("should return a successful response with max depth option for a valid crawl job", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://www.scrapethissite.com", - crawlerOptions: { maxDepth: 1 }, - }); - expect(crawlResponse.statusCode).toBe(200); - - const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - expect(["active", "waiting"]).toContain(response.body.status); - // wait for 60 seconds - let isCompleted = false; - while (!isCompleted) { - const statusCheckResponse = await request(TEST_URL) + const completedResponse = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(statusCheckResponse.statusCode).toBe(200); - isCompleted = statusCheckResponse.body.status === "completed"; - if (!isCompleted) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again - } - } - const completedResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(1); + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(1); - // Check if all URLs have a maximum depth of 1 - urls.forEach((url: string) => { - const pathSplits = new URL(url).pathname.split('/'); - const depth = pathSplits.length - (pathSplits[0].length === 0 && pathSplits[pathSplits.length - 1].length === 0 ? 1 : 0); - expect(depth).toBeLessThanOrEqual(2); - }); - }, 180000); - - it.concurrent("should return a successful response with relative max depth option for a valid crawl job", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://www.scrapethissite.com/pages/", - crawlerOptions: { maxDepth: 1 }, + // Check if all URLs have an absolute maximum depth of 3 after the base URL depth was 2 and the maxDepth was 1 + urls.forEach((url: string) => { + const pathSplits = new URL(url).pathname.split("/"); + const depth = + pathSplits.length - + (pathSplits[0].length === 0 && + pathSplits[pathSplits.length - 1].length === 0 + ? 1 + : 0); + expect(depth).toBeLessThanOrEqual(3); }); - expect(crawlResponse.statusCode).toBe(200); + }, + 180000 + ); - const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - expect(["active", "waiting"]).toContain(response.body.status); - // wait for 60 seconds - let isCompleted = false; - while (!isCompleted) { - const statusCheckResponse = await request(TEST_URL) + it.concurrent( + "should return a successful response with relative max depth option for a valid crawl job with maxDepths equals to zero", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://www.mendable.ai", + crawlerOptions: { maxDepth: 0 } + }); + expect(crawlResponse.statusCode).toBe(200); + + const response = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(statusCheckResponse.statusCode).toBe(200); - isCompleted = statusCheckResponse.body.status === "completed"; - if (!isCompleted) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + expect(["active", "waiting"]).toContain(response.body.status); + // wait for 60 seconds + let isCompleted = false; + while (!isCompleted) { + const statusCheckResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(statusCheckResponse.statusCode).toBe(200); + isCompleted = statusCheckResponse.body.status === "completed"; + if (!isCompleted) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } } - } - const completedResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(1); - - // Check if all URLs have an absolute maximum depth of 3 after the base URL depth was 2 and the maxDepth was 1 - urls.forEach((url: string) => { - const pathSplits = new URL(url).pathname.split('/'); - const depth = pathSplits.length - (pathSplits[0].length === 0 && pathSplits[pathSplits.length - 1].length === 0 ? 1 : 0); - expect(depth).toBeLessThanOrEqual(3); - }); - }, 180000); - - it.concurrent("should return a successful response with relative max depth option for a valid crawl job with maxDepths equals to zero", async () => { - - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://www.mendable.ai", - crawlerOptions: { maxDepth: 0 }, - }); - expect(crawlResponse.statusCode).toBe(200); - - const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - expect(["active", "waiting"]).toContain(response.body.status); - // wait for 60 seconds - let isCompleted = false; - while (!isCompleted) { - const statusCheckResponse = await request(TEST_URL) + const completedResponse = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(statusCheckResponse.statusCode).toBe(200); - isCompleted = statusCheckResponse.body.status === "completed"; - if (!isCompleted) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again - } - } - const completedResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); const testurls = completedResponse.body.data.map( (item: any) => item.metadata?.sourceURL ); //console.log(testurls) - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThanOrEqual(1); + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThanOrEqual(1); - // Check if all URLs have an absolute maximum depth of 3 after the base URL depth was 2 and the maxDepth was 1 - urls.forEach((url: string) => { - const pathSplits = new URL(url).pathname.split('/'); - const depth = pathSplits.length - (pathSplits[0].length === 0 && pathSplits[pathSplits.length - 1].length === 0 ? 1 : 0); - expect(depth).toBeLessThanOrEqual(1); - }); - }, 180000); - - - - + // Check if all URLs have an absolute maximum depth of 3 after the base URL depth was 2 and the maxDepth was 1 + urls.forEach((url: string) => { + const pathSplits = new URL(url).pathname.split("/"); + const depth = + pathSplits.length - + (pathSplits[0].length === 0 && + pathSplits[pathSplits.length - 1].length === 0 + ? 1 + : 0); + expect(depth).toBeLessThanOrEqual(1); + }); + }, + 180000 + ); // it.concurrent("should return a successful response with a valid API key and valid limit option", async () => { // const crawlResponse = await request(TEST_URL) @@ -735,7 +889,7 @@ describe("E2E Tests for API Routes", () => { // url: "https://mendable.ai", // crawlerOptions: { limit: 10 }, // }); - + // const response = await request(TEST_URL) // .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) // .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); @@ -771,100 +925,126 @@ describe("E2E Tests for API Routes", () => { // expect(completedResponse.body.data[0].content).not.toContain("main menu"); // }, 60000); // 60 seconds - it.concurrent("should return a successful response for a valid crawl job with includeHtml set to true option", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true }, - }); - expect(crawlResponse.statusCode).toBe(200); + it.concurrent( + "should return a successful response for a valid crawl job with includeHtml set to true option", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://roastmywebsite.ai", + pageOptions: { includeHtml: true } + }); + expect(crawlResponse.statusCode).toBe(200); - const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - expect(["active", "waiting"]).toContain(response.body.status); - - let isCompleted = false; - while (!isCompleted) { - const statusCheckResponse = await request(TEST_URL) + const response = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(statusCheckResponse.statusCode).toBe(200); - isCompleted = statusCheckResponse.body.status === "completed"; - if (!isCompleted) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + expect(["active", "waiting"]).toContain(response.body.status); + + let isCompleted = false; + while (!isCompleted) { + const statusCheckResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(statusCheckResponse.statusCode).toBe(200); + isCompleted = statusCheckResponse.body.status === "completed"; + if (!isCompleted) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } } - } - const completedResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); - - // 120 seconds - expect(completedResponse.body.data[0]).toHaveProperty("html"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].content).toContain("_Roast_"); - expect(completedResponse.body.data[0].markdown).toContain("_Roast_"); - expect(completedResponse.body.data[0].html).toContain(" { - const crawlInitResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai", - crawlerOptions: { - allowExternalContentLinks: true, - ignoreSitemap: true, - returnOnlyUrls: true, - limit: 50 - } - }); - - expect(crawlInitResponse.statusCode).toBe(200); - expect(crawlInitResponse.body).toHaveProperty("jobId"); - - let crawlStatus: string = "scraping"; - let crawlData = []; - while (crawlStatus !== "completed") { - const statusResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlInitResponse.body.jobId}`) + const completedResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - crawlStatus = statusResponse.body.status; - if (statusResponse.body.data) { - crawlData = statusResponse.body.data; + + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); + expect( + completedResponse.body.data[0].metadata.pageError + ).toBeUndefined(); + + // 120 seconds + expect(completedResponse.body.data[0]).toHaveProperty("html"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].content).toContain("_Roast_"); + expect(completedResponse.body.data[0].markdown).toContain("_Roast_"); + expect(completedResponse.body.data[0].html).toContain(" { + const crawlInitResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://mendable.ai", + crawlerOptions: { + allowExternalContentLinks: true, + ignoreSitemap: true, + returnOnlyUrls: true, + limit: 50 + } + }); + + expect(crawlInitResponse.statusCode).toBe(200); + expect(crawlInitResponse.body).toHaveProperty("jobId"); + + let crawlStatus: string = "scraping"; + let crawlData = []; + while (crawlStatus !== "completed") { + const statusResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlInitResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + crawlStatus = statusResponse.body.status; + if (statusResponse.body.data) { + crawlData = statusResponse.body.data; + } + if (crawlStatus !== "completed") { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } } - if (crawlStatus !== "completed") { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again - } - } - expect(crawlData.length).toBeGreaterThan(0); - expect(crawlData).toEqual(expect.arrayContaining([ - expect.objectContaining({ url: expect.stringContaining("https://firecrawl.dev/?ref=mendable+banner") }), - expect.objectContaining({ url: expect.stringContaining("https://mendable.ai/pricing") }), - expect.objectContaining({ url: expect.stringContaining("https://x.com/CalebPeffer") }) - ])); - }, 180000); // 3 minutes timeout + expect(crawlData.length).toBeGreaterThan(0); + expect(crawlData).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + url: expect.stringContaining( + "https://firecrawl.dev/?ref=mendable+banner" + ) + }), + expect.objectContaining({ + url: expect.stringContaining("https://mendable.ai/pricing") + }), + expect.objectContaining({ + url: expect.stringContaining("https://x.com/CalebPeffer") + }) + ]) + ); + }, + 180000 + ); // 3 minutes timeout }); describe("POST /v0/crawlWebsitePreview", () => { @@ -873,14 +1053,17 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(401); }); - it.concurrent("should return an error response with an invalid API key", async () => { - const response = await request(TEST_URL) - .post("/v0/crawlWebsitePreview") - .set("Authorization", `Bearer invalid-api-key`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(401); - }); + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response = await request(TEST_URL) + .post("/v0/crawlWebsitePreview") + .set("Authorization", `Bearer invalid-api-key`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + expect(response.statusCode).toBe(401); + } + ); // it.concurrent("should return an error for a blocklisted URL", async () => { // const blocklistedUrl = "https://instagram.com/fake-test"; @@ -894,15 +1077,19 @@ describe("E2E Tests for API Routes", () => { // expect(response.body.error).toContain("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); // }); - it.concurrent("should return a timeout error when scraping takes longer than the specified timeout", async () => { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev", timeout: 1000 }); + it.concurrent( + "should return a timeout error when scraping takes longer than the specified timeout", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev", timeout: 1000 }); - expect(response.statusCode).toBe(408); - }, 3000); + expect(response.statusCode).toBe(408); + }, + 3000 + ); // it.concurrent("should return a successful response with a valid API key for crawlWebsitePreview", async () => { // const response = await request(TEST_URL) @@ -924,26 +1111,33 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(401); }); - it.concurrent("should return an error response with an invalid API key", async () => { - const response = await request(TEST_URL) - .post("/v0/search") - .set("Authorization", `Bearer invalid-api-key`) - .set("Content-Type", "application/json") - .send({ query: "test" }); - expect(response.statusCode).toBe(401); - }); + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response = await request(TEST_URL) + .post("/v0/search") + .set("Authorization", `Bearer invalid-api-key`) + .set("Content-Type", "application/json") + .send({ query: "test" }); + expect(response.statusCode).toBe(401); + } + ); - it.concurrent("should return a successful response with a valid API key for search", async () => { - const response = await request(TEST_URL) - .post("/v0/search") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ query: "test" }); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("success"); - expect(response.body.success).toBe(true); - expect(response.body).toHaveProperty("data"); - }, 30000); // 30 seconds timeout + it.concurrent( + "should return a successful response with a valid API key for search", + async () => { + const response = await request(TEST_URL) + .post("/v0/search") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ query: "test" }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("success"); + expect(response.body.success).toBe(true); + expect(response.body).toHaveProperty("data"); + }, + 30000 + ); // 30 seconds timeout }); describe("GET /v0/crawl/status/:jobId", () => { @@ -952,123 +1146,217 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(401); }); - it.concurrent("should return an error response with an invalid API key", async () => { - const response = await request(TEST_URL) - .get("/v0/crawl/status/123") - .set("Authorization", `Bearer invalid-api-key`); - expect(response.statusCode).toBe(401); - }); - - it.concurrent("should return Job not found for invalid job ID", async () => { - const response = await request(TEST_URL) - .get("/v0/crawl/status/invalidJobId") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(404); - }); - - it.concurrent("should return a successful crawl status response for a valid crawl job", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://mendable.ai/blog" }); - expect(crawlResponse.statusCode).toBe(200); - - let isCompleted = false; - let completedResponse; - - while (!isCompleted) { + it.concurrent( + "should return an error response with an invalid API key", + async () => { const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .get("/v0/crawl/status/123") + .set("Authorization", `Bearer invalid-api-key`); + expect(response.statusCode).toBe(401); + } + ); + + it.concurrent( + "should return Job not found for invalid job ID", + async () => { + const response = await request(TEST_URL) + .get("/v0/crawl/status/invalidJobId") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - - if (response.body.status === "completed") { - isCompleted = true; - completedResponse = response; - } else { - await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again - } + expect(response.statusCode).toBe(404); } - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].content).toContain("Mendable"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); + ); - const childrenLinks = completedResponse.body.data.filter(doc => - doc.metadata && doc.metadata.sourceURL && doc.metadata.sourceURL.includes("mendable.ai/blog") - ); + it.concurrent( + "should return a successful crawl status response for a valid crawl job", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://mendable.ai/blog" }); + expect(crawlResponse.statusCode).toBe(200); - expect(childrenLinks.length).toBe(completedResponse.body.data.length); - }, 180000); // 120 seconds - - it.concurrent('should return a successful response for a valid crawl job with PDF files without explicit .pdf extension ', async () => { - const crawlResponse = await request(TEST_URL) - .post('/v0/crawl') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://arxiv.org/pdf/astro-ph/9301001', crawlerOptions: { limit: 10, excludes: [ 'list/*', 'login', 'abs/*', 'static/*', 'about/*', 'archive/*' ] }}); - expect(crawlResponse.statusCode).toBe(200); + let isCompleted = false; + let completedResponse; - let isCompleted = false; - let completedResponse; + while (!isCompleted) { + const response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); - while (!isCompleted) { - const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('status'); - - if (response.body.status === 'completed') { - isCompleted = true; - completedResponse = response; - } else { - await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again + if (response.body.status === "completed") { + isCompleted = true; + completedResponse = response; + } else { + await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again + } } - } - expect(completedResponse.body.status).toBe('completed'); - expect(completedResponse.body).toHaveProperty('data'); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].content).toContain("Mendable"); + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); + expect( + completedResponse.body.data[0].metadata.pageError + ).toBeUndefined(); + + const childrenLinks = completedResponse.body.data.filter( + (doc) => + doc.metadata && + doc.metadata.sourceURL && + doc.metadata.sourceURL.includes("mendable.ai/blog") + ); + + expect(childrenLinks.length).toBe(completedResponse.body.data.length); + }, + 180000 + ); // 120 seconds + + it.concurrent( + "should return a successful response for a valid crawl job with PDF files without explicit .pdf extension ", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://arxiv.org/pdf/astro-ph/9301001", + crawlerOptions: { + limit: 10, + excludes: [ + "list/*", + "login", + "abs/*", + "static/*", + "about/*", + "archive/*" + ] + } + }); + expect(crawlResponse.statusCode).toBe(200); + + let isCompleted = false; + let completedResponse; + + while (!isCompleted) { + const response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + + if (response.body.status === "completed") { + isCompleted = true; + completedResponse = response; + } else { + await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again + } + } + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); expect(completedResponse.body.data.length).toEqual(1); expect(completedResponse.body.data).toEqual( expect.arrayContaining([ expect.objectContaining({ - content: expect.stringContaining('asymmetries might represent, for instance, preferred source orientations to our line of sight.') + content: expect.stringContaining( + "asymmetries might represent, for instance, preferred source orientations to our line of sight." + ) }) ]) ); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); - }, 180000); // 120 seconds + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); + expect( + completedResponse.body.data[0].metadata.pageError + ).toBeUndefined(); + }, + 180000 + ); // 120 seconds + it.concurrent( + "should return a successful response for a valid crawl job with includeHtml set to true option (2)", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://roastmywebsite.ai", + pageOptions: { includeHtml: true } + }); + expect(crawlResponse.statusCode).toBe(200); + const response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + expect(["active", "waiting"]).toContain(response.body.status); - it.concurrent("should return a successful response for a valid crawl job with includeHtml set to true option (2)", async () => { + let isFinished = false; + let completedResponse; + + while (!isFinished) { + const response = await request(TEST_URL) + .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + + if (response.body.status === "completed") { + isFinished = true; + completedResponse = response; + } else { + await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again + } + } + + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0]).toHaveProperty("html"); + expect(completedResponse.body.data[0].content).toContain("_Roast_"); + expect(completedResponse.body.data[0].markdown).toContain("_Roast_"); + expect(completedResponse.body.data[0].html).toContain(" { const crawlResponse = await request(TEST_URL) .post("/v0/crawl") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send({ - url: "https://roastmywebsite.ai", + url: "https://mendable.ai/blog", pageOptions: { includeHtml: true }, + crawlerOptions: { allowBackwardCrawling: true } }); expect(crawlResponse.statusCode).toBe(200); - const response = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - expect(["active", "waiting"]).toContain(response.body.status); - let isFinished = false; let completedResponse; @@ -1095,190 +1383,167 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0]).toHaveProperty("html"); - expect(completedResponse.body.data[0].content).toContain("_Roast_"); - expect(completedResponse.body.data[0].markdown).toContain("_Roast_"); - expect(completedResponse.body.data[0].html).toContain(" { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai/blog", - pageOptions: { includeHtml: true }, - crawlerOptions: { allowBackwardCrawling: true }, + const onlyChildrenLinks = completedResponse.body.data.filter((doc) => { + return ( + doc.metadata && + doc.metadata.sourceURL && + doc.metadata.sourceURL.includes("mendable.ai/blog") + ); }); - expect(crawlResponse.statusCode).toBe(200); - - let isFinished = false; - let completedResponse; - while (!isFinished) { - const response = await request(TEST_URL) + expect(completedResponse.body.data.length).toBeGreaterThan( + onlyChildrenLinks.length + ); + }, + 60000 + ); + + it.concurrent( + "If someone cancels a crawl job, it should turn into failed status", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://jestjs.io" }); + + expect(crawlResponse.statusCode).toBe(200); + + await new Promise((r) => setTimeout(r, 20000)); + + const responseCancel = await request(TEST_URL) + .delete(`/v0/crawl/cancel/${crawlResponse.body.jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(responseCancel.statusCode).toBe(200); + expect(responseCancel.body).toHaveProperty("status"); + expect(responseCancel.body.status).toBe("cancelled"); + + await new Promise((r) => setTimeout(r, 10000)); + const completedResponse = await request(TEST_URL) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - if (response.body.status === "completed") { - isFinished = true; - completedResponse = response; - } else { - await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again - } - } - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0]).toHaveProperty("html"); - expect(completedResponse.body.data[0].content).toContain("Mendable"); - expect(completedResponse.body.data[0].markdown).toContain("Mendable"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.pageError).toBeUndefined(); - - const onlyChildrenLinks = completedResponse.body.data.filter(doc => { - return doc.metadata && doc.metadata.sourceURL && doc.metadata.sourceURL.includes("mendable.ai/blog") - }); - - expect(completedResponse.body.data.length).toBeGreaterThan(onlyChildrenLinks.length); - }, 60000); - - it.concurrent("If someone cancels a crawl job, it should turn into failed status", async () => { - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://jestjs.io" }); - - expect(crawlResponse.statusCode).toBe(200); - - await new Promise((r) => setTimeout(r, 20000)); - - const responseCancel = await request(TEST_URL) - .delete(`/v0/crawl/cancel/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(responseCancel.statusCode).toBe(200); - expect(responseCancel.body).toHaveProperty("status"); - expect(responseCancel.body.status).toBe("cancelled"); - - await new Promise((r) => setTimeout(r, 10000)); - const completedResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("failed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data).toBeNull(); - expect(completedResponse.body).toHaveProperty("partial_data"); - expect(completedResponse.body.partial_data[0]).toHaveProperty("content"); - expect(completedResponse.body.partial_data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.partial_data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.partial_data[0].metadata.pageStatusCode).toBe(200); - expect(completedResponse.body.partial_data[0].metadata.pageError).toBeUndefined(); - }, 60000); // 60 seconds + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("failed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data).toBeNull(); + expect(completedResponse.body).toHaveProperty("partial_data"); + expect(completedResponse.body.partial_data[0]).toHaveProperty("content"); + expect(completedResponse.body.partial_data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.partial_data[0]).toHaveProperty("metadata"); + expect( + completedResponse.body.partial_data[0].metadata.pageStatusCode + ).toBe(200); + expect( + completedResponse.body.partial_data[0].metadata.pageError + ).toBeUndefined(); + }, + 60000 + ); // 60 seconds describe("POST /v0/scrape with LLM Extraction", () => { - it.concurrent("should extract data using LLM extraction mode", async () => { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai", - pageOptions: { - onlyMainContent: true, - }, - extractorOptions: { - mode: "llm-extraction", - extractionPrompt: - "Based on the information on the page, find what the company's mission is and whether it supports SSO, and whether it is open source", - extractionSchema: { - type: "object", - properties: { - company_mission: { - type: "string", - }, - supports_sso: { - type: "boolean", - }, - is_open_source: { - type: "boolean", - }, - }, - required: ["company_mission", "supports_sso", "is_open_source"], + it.concurrent( + "should extract data using LLM extraction mode", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://mendable.ai", + pageOptions: { + onlyMainContent: true }, - }, - }); - - // Ensure that the job was successfully created before proceeding with LLM extraction - expect(response.statusCode).toBe(200); - - // Assuming the LLM extraction object is available in the response body under `data.llm_extraction` - let llmExtraction = response.body.data.llm_extraction; - - // Check if the llm_extraction object has the required properties with correct types and values - expect(llmExtraction).toHaveProperty("company_mission"); - expect(typeof llmExtraction.company_mission).toBe("string"); - expect(llmExtraction).toHaveProperty("supports_sso"); - expect(llmExtraction.supports_sso).toBe(true); - expect(typeof llmExtraction.supports_sso).toBe("boolean"); - expect(llmExtraction).toHaveProperty("is_open_source"); - expect(llmExtraction.is_open_source).toBe(false); - expect(typeof llmExtraction.is_open_source).toBe("boolean"); - }, 60000); // 60 secs - - it.concurrent("should extract data using LLM extraction mode with RawHtml", async () => { - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://mendable.ai", - - extractorOptions: { - mode: "llm-extraction-from-raw-html", - extractionPrompt: - "Based on the information on the page, what are the primary and secondary CTA buttons?", - extractionSchema: { - type: "object", - properties: { - primary_cta: { - type: "string", + extractorOptions: { + mode: "llm-extraction", + extractionPrompt: + "Based on the information on the page, find what the company's mission is and whether it supports SSO, and whether it is open source", + extractionSchema: { + type: "object", + properties: { + company_mission: { + type: "string" + }, + supports_sso: { + type: "boolean" + }, + is_open_source: { + type: "boolean" + } }, - secondary_cta: { - type: "string", + required: ["company_mission", "supports_sso", "is_open_source"] + } + } + }); + + // Ensure that the job was successfully created before proceeding with LLM extraction + expect(response.statusCode).toBe(200); + + // Assuming the LLM extraction object is available in the response body under `data.llm_extraction` + let llmExtraction = response.body.data.llm_extraction; + + // Check if the llm_extraction object has the required properties with correct types and values + expect(llmExtraction).toHaveProperty("company_mission"); + expect(typeof llmExtraction.company_mission).toBe("string"); + expect(llmExtraction).toHaveProperty("supports_sso"); + expect(llmExtraction.supports_sso).toBe(true); + expect(typeof llmExtraction.supports_sso).toBe("boolean"); + expect(llmExtraction).toHaveProperty("is_open_source"); + expect(llmExtraction.is_open_source).toBe(false); + expect(typeof llmExtraction.is_open_source).toBe("boolean"); + }, + 60000 + ); // 60 secs + + it.concurrent( + "should extract data using LLM extraction mode with RawHtml", + async () => { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://mendable.ai", + + extractorOptions: { + mode: "llm-extraction-from-raw-html", + extractionPrompt: + "Based on the information on the page, what are the primary and secondary CTA buttons?", + extractionSchema: { + type: "object", + properties: { + primary_cta: { + type: "string" + }, + secondary_cta: { + type: "string" + } }, - }, - required: ["primary_cta", "secondary_cta"], - }, - }, - }); + required: ["primary_cta", "secondary_cta"] + } + } + }); - // Ensure that the job was successfully created before proceeding with LLM extraction - expect(response.statusCode).toBe(200); + // Ensure that the job was successfully created before proceeding with LLM extraction + expect(response.statusCode).toBe(200); - // Assuming the LLM extraction object is available in the response body under `data.llm_extraction` - let llmExtraction = response.body.data.llm_extraction; + // Assuming the LLM extraction object is available in the response body under `data.llm_extraction` + let llmExtraction = response.body.data.llm_extraction; - // Check if the llm_extraction object has the required properties with correct types and values - expect(llmExtraction).toHaveProperty("primary_cta"); - expect(typeof llmExtraction.primary_cta).toBe("string"); - expect(llmExtraction).toHaveProperty("secondary_cta"); - expect(typeof llmExtraction.secondary_cta).toBe("string"); - - }, 60000); // 60 secs + // Check if the llm_extraction object has the required properties with correct types and values + expect(llmExtraction).toHaveProperty("primary_cta"); + expect(typeof llmExtraction.primary_cta).toBe("string"); + expect(llmExtraction).toHaveProperty("secondary_cta"); + expect(typeof llmExtraction.secondary_cta).toBe("string"); + }, + 60000 + ); // 60 secs }); // describe("POST /v0/scrape for Top 100 Companies", () => { @@ -1340,60 +1605,63 @@ describe("E2E Tests for API Routes", () => { // }); describe("POST /v0/crawl with fast mode", () => { - it.concurrent("should complete the crawl under 20 seconds", async () => { - const startTime = Date.now(); + it.concurrent( + "should complete the crawl under 20 seconds", + async () => { + const startTime = Date.now(); - const crawlResponse = await request(TEST_URL) - .post("/v0/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://flutterbricks.com", - crawlerOptions: { - mode: "fast" + const crawlResponse = await request(TEST_URL) + .post("/v0/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://flutterbricks.com", + crawlerOptions: { + mode: "fast" + } + }); + + expect(crawlResponse.statusCode).toBe(200); + + const jobId = crawlResponse.body.jobId; + let statusResponse; + let isFinished = false; + + while (!isFinished) { + statusResponse = await request(TEST_URL) + .get(`/v0/crawl/status/${jobId}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(statusResponse.statusCode).toBe(200); + isFinished = statusResponse.body.status === "completed"; + + if (!isFinished) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again } - }); - - expect(crawlResponse.statusCode).toBe(200); - - const jobId = crawlResponse.body.jobId; - let statusResponse; - let isFinished = false; - - while (!isFinished) { - statusResponse = await request(TEST_URL) - .get(`/v0/crawl/status/${jobId}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - - expect(statusResponse.statusCode).toBe(200); - isFinished = statusResponse.body.status === "completed"; - - if (!isFinished) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again } - } - // const endTime = Date.now(); - // const timeElapsed = (endTime - startTime) / 1000; // Convert to seconds + // const endTime = Date.now(); + // const timeElapsed = (endTime - startTime) / 1000; // Convert to seconds - // console.log(`Time elapsed: ${timeElapsed} seconds`); + // console.log(`Time elapsed: ${timeElapsed} seconds`); - expect(statusResponse.body.status).toBe("completed"); - expect(statusResponse.body).toHaveProperty("data"); - expect(statusResponse.body.data[0]).toHaveProperty("content"); - expect(statusResponse.body.data[0]).toHaveProperty("markdown"); - expect(statusResponse.body.data[0]).toHaveProperty("metadata"); - expect(statusResponse.body.data[0].metadata.pageStatusCode).toBe(200); - expect(statusResponse.body.data[0].metadata.pageError).toBeUndefined(); + expect(statusResponse.body.status).toBe("completed"); + expect(statusResponse.body).toHaveProperty("data"); + expect(statusResponse.body.data[0]).toHaveProperty("content"); + expect(statusResponse.body.data[0]).toHaveProperty("markdown"); + expect(statusResponse.body.data[0]).toHaveProperty("metadata"); + expect(statusResponse.body.data[0].metadata.pageStatusCode).toBe(200); + expect(statusResponse.body.data[0].metadata.pageError).toBeUndefined(); - const results = statusResponse.body.data; - // results.forEach((result, i) => { - // console.log(result.metadata.sourceURL); - // }); - expect(results.length).toBeGreaterThanOrEqual(10); - expect(results.length).toBeLessThanOrEqual(15); - - }, 20000); + const results = statusResponse.body.data; + // results.forEach((result, i) => { + // console.log(result.metadata.sourceURL); + // }); + expect(results.length).toBeGreaterThanOrEqual(10); + expect(results.length).toBeLessThanOrEqual(15); + }, + 20000 + ); // it.concurrent("should complete the crawl in more than 10 seconds", async () => { // const startTime = Date.now(); @@ -1440,7 +1708,7 @@ describe("E2E Tests for API Routes", () => { // // }); // expect(results.length).toBeGreaterThanOrEqual(10); // expect(results.length).toBeLessThanOrEqual(15); - + // }, 50000);// 15 seconds timeout to account for network delays }); @@ -1453,24 +1721,28 @@ describe("E2E Tests for API Routes", () => { }); describe("Rate Limiter", () => { - it.concurrent("should return 429 when rate limit is exceeded for preview token", async () => { - for (let i = 0; i < 5; i++) { + it.concurrent( + "should return 429 when rate limit is exceeded for preview token", + async () => { + for (let i = 0; i < 5; i++) { + const response = await request(TEST_URL) + .post("/v0/scrape") + .set("Authorization", `Bearer this_is_just_a_preview_token`) + .set("Content-Type", "application/json") + .send({ url: "https://www.scrapethissite.com" }); + + expect(response.statusCode).toBe(200); + } const response = await request(TEST_URL) .post("/v0/scrape") .set("Authorization", `Bearer this_is_just_a_preview_token`) .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com" }); - expect(response.statusCode).toBe(200); - } - const response = await request(TEST_URL) - .post("/v0/scrape") - .set("Authorization", `Bearer this_is_just_a_preview_token`) - .set("Content-Type", "application/json") - .send({ url: "https://www.scrapethissite.com" }); - - expect(response.statusCode).toBe(429); - }, 90000); + expect(response.statusCode).toBe(429); + }, + 90000 + ); }); // it.concurrent("should return 429 when rate limit is exceeded for API key", async () => { diff --git a/apps/api/src/__tests__/e2e_map/index.test.ts b/apps/api/src/__tests__/e2e_map/index.test.ts index b065dff1..948f097e 100644 --- a/apps/api/src/__tests__/e2e_map/index.test.ts +++ b/apps/api/src/__tests__/e2e_map/index.test.ts @@ -15,7 +15,7 @@ describe("E2E Tests for Map API Routes", () => { .send({ url: "https://firecrawl.dev", sitemapOnly: false, - search: "smart-crawl", + search: "smart-crawl" }); console.log(response.body); @@ -37,7 +37,7 @@ describe("E2E Tests for Map API Routes", () => { .send({ url: "https://firecrawl.dev", sitemapOnly: false, - includeSubdomains: true, + includeSubdomains: true }); console.log(response.body); @@ -60,7 +60,7 @@ describe("E2E Tests for Map API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev", - sitemapOnly: true, + sitemapOnly: true }); console.log(response.body); @@ -84,7 +84,7 @@ describe("E2E Tests for Map API Routes", () => { .send({ url: "https://firecrawl.dev", sitemapOnly: false, - limit: 10, + limit: 10 }); console.log(response.body); @@ -104,7 +104,7 @@ describe("E2E Tests for Map API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://geekflare.com/sitemap_index.xml", - sitemapOnly: true, + sitemapOnly: true }); console.log(response.body); diff --git a/apps/api/src/__tests__/e2e_noAuth/index.test.ts b/apps/api/src/__tests__/e2e_noAuth/index.test.ts index 83f676b8..9c3ddf33 100644 --- a/apps/api/src/__tests__/e2e_noAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_noAuth/index.test.ts @@ -32,7 +32,6 @@ describe("E2E Tests for API Routes with No Authentication", () => { process.env = originalEnv; }); - describe("GET /", () => { it("should return Hello, world! message", async () => { const response = await request(TEST_URL).get("/"); @@ -62,7 +61,9 @@ describe("E2E Tests for API Routes with No Authentication", () => { .set("Content-Type", "application/json") .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); - expect(response.body.error).toContain("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); + expect(response.body.error).toContain( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); }); it("should return a successful response", async () => { @@ -87,7 +88,9 @@ describe("E2E Tests for API Routes with No Authentication", () => { .set("Content-Type", "application/json") .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); - expect(response.body.error).toContain("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); + expect(response.body.error).toContain( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); }); it("should return a successful response", async () => { @@ -116,7 +119,9 @@ describe("E2E Tests for API Routes with No Authentication", () => { .set("Content-Type", "application/json") .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); - expect(response.body.error).toContain("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); + expect(response.body.error).toContain( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); }); it("should return a successful response", async () => { @@ -199,8 +204,6 @@ describe("E2E Tests for API Routes with No Authentication", () => { expect(completedResponse.body.data[0]).toHaveProperty("content"); expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - - }, 60000); // 60 seconds }); diff --git a/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts b/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts index e1f5f3fa..33e3be5d 100644 --- a/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts @@ -2,7 +2,7 @@ import request from "supertest"; import { configDotenv } from "dotenv"; import { ScrapeRequestInput, - ScrapeResponseRequestTest, + ScrapeResponseRequestTest } from "../../controllers/v1/types"; configDotenv(); @@ -19,15 +19,17 @@ describe("E2E Tests for v1 API Routes", () => { describe("GET /is-production", () => { it.concurrent("should return the production status", async () => { - const response: ScrapeResponseRequestTest = await request(TEST_URL).get( - "/is-production" - ); + const response: ScrapeResponseRequestTest = + await request(TEST_URL).get("/is-production"); - console.log('process.env.USE_DB_AUTHENTICATION', process.env.USE_DB_AUTHENTICATION); - console.log('?', process.env.USE_DB_AUTHENTICATION === 'true'); - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; - console.log('!!useDbAuthentication', !!useDbAuthentication); - console.log('!useDbAuthentication', !useDbAuthentication); + console.log( + "process.env.USE_DB_AUTHENTICATION", + process.env.USE_DB_AUTHENTICATION + ); + console.log("?", process.env.USE_DB_AUTHENTICATION === "true"); + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; + console.log("!!useDbAuthentication", !!useDbAuthentication); + console.log("!useDbAuthentication", !useDbAuthentication); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("isProduction"); @@ -37,15 +39,15 @@ describe("E2E Tests for v1 API Routes", () => { describe("POST /v1/scrape", () => { it.concurrent("should require authorization", async () => { const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/scrape") - .send({ url: "https://firecrawl.dev"}) + .post("/v1/scrape") + .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); }); it.concurrent("should throw error for blocklisted URL", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://facebook.com/fake-test", + url: "https://facebook.com/fake-test" }; const response = await request(TEST_URL) @@ -55,7 +57,9 @@ describe("E2E Tests for v1 API Routes", () => { .send(scrapeRequest); expect(response.statusCode).toBe(403); - expect(response.body.error).toBe("URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions."); + expect(response.body.error).toBe( + "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions." + ); }); it.concurrent( @@ -74,7 +78,7 @@ describe("E2E Tests for v1 API Routes", () => { "should return a successful response with a valid API key", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://roastmywebsite.ai", + url: "https://roastmywebsite.ai" }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -126,7 +130,7 @@ describe("E2E Tests for v1 API Routes", () => { "should return a successful response with a valid API key", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://arxiv.org/abs/2410.04840", + url: "https://arxiv.org/abs/2410.04840" }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -146,8 +150,12 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).not.toHaveProperty("html"); expect(response.body.data.markdown).toContain("Strong Model Collapse"); expect(response.body.data.metadata.error).toBeUndefined(); - expect(response.body.data.metadata.description).toContain("Abstract page for arXiv paper 2410.04840: Strong Model Collapse"); - expect(response.body.data.metadata.citation_title).toBe("Strong Model Collapse"); + expect(response.body.data.metadata.description).toContain( + "Abstract page for arXiv paper 2410.04840: Strong Model Collapse" + ); + expect(response.body.data.metadata.citation_title).toBe( + "Strong Model Collapse" + ); expect(response.body.data.metadata.citation_author).toEqual([ "Dohmatob, Elvis", "Feng, Yunzhen", @@ -155,11 +163,21 @@ describe("E2E Tests for v1 API Routes", () => { "Kempe, Julia" ]); expect(response.body.data.metadata.citation_date).toBe("2024/10/07"); - expect(response.body.data.metadata.citation_online_date).toBe("2024/10/08"); - expect(response.body.data.metadata.citation_pdf_url).toBe("http://arxiv.org/pdf/2410.04840"); - expect(response.body.data.metadata.citation_arxiv_id).toBe("2410.04840"); - expect(response.body.data.metadata.citation_abstract).toContain("Within the scaling laws paradigm"); - expect(response.body.data.metadata.sourceURL).toBe("https://arxiv.org/abs/2410.04840"); + expect(response.body.data.metadata.citation_online_date).toBe( + "2024/10/08" + ); + expect(response.body.data.metadata.citation_pdf_url).toBe( + "http://arxiv.org/pdf/2410.04840" + ); + expect(response.body.data.metadata.citation_arxiv_id).toBe( + "2410.04840" + ); + expect(response.body.data.metadata.citation_abstract).toContain( + "Within the scaling laws paradigm" + ); + expect(response.body.data.metadata.sourceURL).toBe( + "https://arxiv.org/abs/2410.04840" + ); expect(response.body.data.metadata.statusCode).toBe(200); }, 30000 @@ -169,7 +187,7 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest: ScrapeRequestInput = { url: "https://roastmywebsite.ai", - formats: ["markdown", "html"], + formats: ["markdown", "html"] }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -177,7 +195,7 @@ describe("E2E Tests for v1 API Routes", () => { .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { @@ -193,62 +211,77 @@ describe("E2E Tests for v1 API Routes", () => { }, 30000 ); - it.concurrent('should return a successful response for a valid scrape with PDF file', async () => { + it.concurrent( + "should return a successful response for a valid scrape with PDF file", + async () => { const scrapeRequest: ScrapeRequestInput = { url: "https://arxiv.org/pdf/astro-ph/9301001.pdf" - // formats: ["markdown", "html"], + // formats: ["markdown", "html"], }; const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post('/v1/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send(scrapeRequest); - await new Promise((r) => setTimeout(r, 6000)); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.markdown).toContain('Broad Line Radio Galaxy'); - expect(response.body.data.metadata.statusCode).toBe(200); - expect(response.body.data.metadata.error).toBeUndefined(); - }, 60000); - - it.concurrent('should return a successful response for a valid scrape with PDF file without explicit .pdf extension', async () => { - const scrapeRequest: ScrapeRequestInput = { - url: "https://arxiv.org/pdf/astro-ph/9301001" - }; - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post('/v1/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send(scrapeRequest); - await new Promise((r) => setTimeout(r, 6000)); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - expect(response.body.data).toHaveProperty('markdown'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.markdown).toContain('Broad Line Radio Galaxy'); - expect(response.body.data.metadata.statusCode).toBe(200); - expect(response.body.data.metadata.error).toBeUndefined(); - }, 60000); - - it.concurrent("should return a successful response with a valid API key with removeTags option", async () => { - const scrapeRequest: ScrapeRequestInput = { - url: "https://www.scrapethissite.com/", - onlyMainContent: false // default is true - }; - const responseWithoutRemoveTags: ScrapeResponseRequestTest = await request(TEST_URL) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); + await new Promise((r) => setTimeout(r, 6000)); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.markdown).toContain( + "Broad Line Radio Galaxy" + ); + expect(response.body.data.metadata.statusCode).toBe(200); + expect(response.body.data.metadata.error).toBeUndefined(); + }, + 60000 + ); + + it.concurrent( + "should return a successful response for a valid scrape with PDF file without explicit .pdf extension", + async () => { + const scrapeRequest: ScrapeRequestInput = { + url: "https://arxiv.org/pdf/astro-ph/9301001" + }; + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequest); + await new Promise((r) => setTimeout(r, 6000)); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.markdown).toContain( + "Broad Line Radio Galaxy" + ); + expect(response.body.data.metadata.statusCode).toBe(200); + expect(response.body.data.metadata.error).toBeUndefined(); + }, + 60000 + ); + + it.concurrent( + "should return a successful response with a valid API key with removeTags option", + async () => { + const scrapeRequest: ScrapeRequestInput = { + url: "https://www.scrapethissite.com/", + onlyMainContent: false // default is true + }; + const responseWithoutRemoveTags: ScrapeResponseRequestTest = + await request(TEST_URL) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequest); expect(responseWithoutRemoveTags.statusCode).toBe(200); expect(responseWithoutRemoveTags.body).toHaveProperty("data"); @@ -258,13 +291,17 @@ describe("E2E Tests for v1 API Routes", () => { expect(responseWithoutRemoveTags.body.data).toHaveProperty("markdown"); expect(responseWithoutRemoveTags.body.data).toHaveProperty("metadata"); expect(responseWithoutRemoveTags.body.data).not.toHaveProperty("html"); - expect(responseWithoutRemoveTags.body.data.markdown).toContain("[FAQ](/faq/)"); // .nav - expect(responseWithoutRemoveTags.body.data.markdown).toContain("Hartley Brody 2023"); // #footer - + expect(responseWithoutRemoveTags.body.data.markdown).toContain( + "[FAQ](/faq/)" + ); // .nav + expect(responseWithoutRemoveTags.body.data.markdown).toContain( + "Hartley Brody 2023" + ); // #footer + const scrapeRequestWithRemoveTags: ScrapeRequestInput = { - url: "https://www.scrapethissite.com/", - excludeTags: ['.nav', '#footer', 'strong'], - onlyMainContent: false // default is true + url: "https://www.scrapethissite.com/", + excludeTags: [".nav", "#footer", "strong"], + onlyMainContent: false // default is true }; const response: ScrapeResponseRequestTest = await request(TEST_URL) .post("/v1/scrape") @@ -281,725 +318,757 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data).not.toHaveProperty("html"); expect(response.body.data.markdown).not.toContain("Hartley Brody 2023"); - expect(response.body.data.markdown).not.toContain("[FAQ](/faq/)"); // - }, 30000); + expect(response.body.data.markdown).not.toContain("[FAQ](/faq/)"); // + }, + 30000 + ); - it.concurrent('should return a successful response for a scrape with 400 page', async () => { + it.concurrent( + "should return a successful response for a scrape with 400 page", + async () => { const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post('/v1/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/400' }); + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/400" }); await new Promise((r) => setTimeout(r, 5000)); - + expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); + expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data).toHaveProperty('markdown'); - expect(response.body.data).toHaveProperty('metadata'); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(400); - }, 60000); + }, + 60000 + ); - - it.concurrent('should return a successful response for a scrape with 401 page', async () => { + it.concurrent( + "should return a successful response for a scrape with 401 page", + async () => { const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post('/v1/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/401' }); + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/401" }); await new Promise((r) => setTimeout(r, 5000)); - + expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); + expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data).toHaveProperty('markdown'); - expect(response.body.data).toHaveProperty('metadata'); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(401); - }, 60000); + }, + 60000 + ); - // Removed it as we want to retry fallback to the next scraper - // it.concurrent('should return a successful response for a scrape with 403 page', async () => { - // const response: ScrapeResponseRequestTest = await request(TEST_URL) - // .post('/v1/scrape') - // .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - // .set('Content-Type', 'application/json') - // .send({ url: 'https://httpstat.us/403' }); - // await new Promise((r) => setTimeout(r, 5000)); - - // expect(response.statusCode).toBe(200); - // expect(response.body).toHaveProperty('data'); - // if (!("data" in response.body)) { - // throw new Error("Expected response body to have 'data' property"); - // } - // expect(response.body.data).toHaveProperty('markdown'); - // expect(response.body.data).toHaveProperty('metadata'); - // expect(response.body.data.metadata.statusCode).toBe(403); - // }, 60000); + // Removed it as we want to retry fallback to the next scraper + // it.concurrent('should return a successful response for a scrape with 403 page', async () => { + // const response: ScrapeResponseRequestTest = await request(TEST_URL) + // .post('/v1/scrape') + // .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) + // .set('Content-Type', 'application/json') + // .send({ url: 'https://httpstat.us/403' }); + // await new Promise((r) => setTimeout(r, 5000)); - it.concurrent('should return a successful response for a scrape with 404 page', async () => { + // expect(response.statusCode).toBe(200); + // expect(response.body).toHaveProperty('data'); + // if (!("data" in response.body)) { + // throw new Error("Expected response body to have 'data' property"); + // } + // expect(response.body.data).toHaveProperty('markdown'); + // expect(response.body.data).toHaveProperty('metadata'); + // expect(response.body.data.metadata.statusCode).toBe(403); + // }, 60000); + + it.concurrent( + "should return a successful response for a scrape with 404 page", + async () => { const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post('/v1/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/404' }); + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/404" }); await new Promise((r) => setTimeout(r, 5000)); - + expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); + expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data).toHaveProperty('markdown'); - expect(response.body.data).toHaveProperty('metadata'); + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(404); - }, 60000); + }, + 60000 + ); - // it.concurrent('should return a successful response for a scrape with 405 page', async () => { - // const response: ScrapeResponseRequestTest = await request(TEST_URL) - // .post('/v1/scrape') - // .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - // .set('Content-Type', 'application/json') - // .send({ url: 'https://httpstat.us/405' }); - // await new Promise((r) => setTimeout(r, 5000)); - - // expect(response.statusCode).toBe(200); - // expect(response.body).toHaveProperty('data'); - // if (!("data" in response.body)) { - // throw new Error("Expected response body to have 'data' property"); - // } - // expect(response.body.data).toHaveProperty('markdown'); - // expect(response.body.data).toHaveProperty('metadata'); - // expect(response.body.data.metadata.statusCode).toBe(405); - // }, 60000); + // it.concurrent('should return a successful response for a scrape with 405 page', async () => { + // const response: ScrapeResponseRequestTest = await request(TEST_URL) + // .post('/v1/scrape') + // .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) + // .set('Content-Type', 'application/json') + // .send({ url: 'https://httpstat.us/405' }); + // await new Promise((r) => setTimeout(r, 5000)); - // it.concurrent('should return a successful response for a scrape with 500 page', async () => { - // const response: ScrapeResponseRequestTest = await request(TEST_URL) - // .post('/v1/scrape') - // .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - // .set('Content-Type', 'application/json') - // .send({ url: 'https://httpstat.us/500' }); - // await new Promise((r) => setTimeout(r, 5000)); - - // expect(response.statusCode).toBe(200); - // expect(response.body).toHaveProperty('data'); - // if (!("data" in response.body)) { - // throw new Error("Expected response body to have 'data' property"); - // } - // expect(response.body.data).toHaveProperty('markdown'); - // expect(response.body.data).toHaveProperty('metadata'); - // expect(response.body.data.metadata.statusCode).toBe(500); - // }, 60000); + // expect(response.statusCode).toBe(200); + // expect(response.body).toHaveProperty('data'); + // if (!("data" in response.body)) { + // throw new Error("Expected response body to have 'data' property"); + // } + // expect(response.body.data).toHaveProperty('markdown'); + // expect(response.body.data).toHaveProperty('metadata'); + // expect(response.body.data.metadata.statusCode).toBe(405); + // }, 60000); - it.concurrent("should return a timeout error when scraping takes longer than the specified timeout", async () => { + // it.concurrent('should return a successful response for a scrape with 500 page', async () => { + // const response: ScrapeResponseRequestTest = await request(TEST_URL) + // .post('/v1/scrape') + // .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) + // .set('Content-Type', 'application/json') + // .send({ url: 'https://httpstat.us/500' }); + // await new Promise((r) => setTimeout(r, 5000)); + + // expect(response.statusCode).toBe(200); + // expect(response.body).toHaveProperty('data'); + // if (!("data" in response.body)) { + // throw new Error("Expected response body to have 'data' property"); + // } + // expect(response.body.data).toHaveProperty('markdown'); + // expect(response.body.data).toHaveProperty('metadata'); + // expect(response.body.data.metadata.statusCode).toBe(500); + // }, 60000); + + it.concurrent( + "should return a timeout error when scraping takes longer than the specified timeout", + async () => { const response: ScrapeResponseRequestTest = await request(TEST_URL) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev", timeout: 1000 }); - + expect(response.statusCode).toBe(408); - }, 3000); + }, + 3000 + ); - it.concurrent( - "should return a successful response with a valid API key and includeHtml set to true", - async () => { - const scrapeRequest: ScrapeRequestInput = { - url: "https://roastmywebsite.ai", - formats: ["html","rawHtml"], - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(scrapeRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - expect(response.body.data).not.toHaveProperty("markdown"); - expect(response.body.data).toHaveProperty("html"); - expect(response.body.data).toHaveProperty("rawHtml"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data.html).toContain(" { + const scrapeRequest: ScrapeRequestInput = { + url: "https://roastmywebsite.ai", + formats: ["html", "rawHtml"] + }; - it.concurrent( - "should return a successful response with waitFor", - async () => { - const scrapeRequest: ScrapeRequestInput = { - url: "https://ycombinator.com/companies", - formats: ["markdown"], - waitFor: 8000 - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(scrapeRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - expect(response.body.data).toHaveProperty("markdown"); - expect(response.body.data).not.toHaveProperty("html"); - expect(response.body.data).not.toHaveProperty("links"); - expect(response.body.data).not.toHaveProperty("rawHtml"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data.markdown).toContain("PagerDuty"); - expect(response.body.data.metadata.statusCode).toBe(200); - expect(response.body.data.metadata.error).toBeUndefined(); + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequest); - }, - 30000 - ); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + expect(response.body.data).not.toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("html"); + expect(response.body.data).toHaveProperty("rawHtml"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.html).toContain(" { - const scrapeRequest: ScrapeRequestInput = { - url: "https://roastmywebsite.ai", - formats: ["links"], - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(scrapeRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - expect(response.body.data).not.toHaveProperty("html"); - expect(response.body.data).not.toHaveProperty("rawHtml"); - expect(response.body.data).toHaveProperty("links"); - expect(response.body.data).toHaveProperty("metadata"); - expect(response.body.data.links).toContain("https://firecrawl.dev"); - expect(response.body.data.metadata.statusCode).toBe(200); - expect(response.body.data.metadata.error).toBeUndefined(); - }, - 30000 - ); - + it.concurrent( + "should return a successful response with waitFor", + async () => { + const scrapeRequest: ScrapeRequestInput = { + url: "https://ycombinator.com/companies", + formats: ["markdown"], + waitFor: 8000 + }; + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequest); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).not.toHaveProperty("html"); + expect(response.body.data).not.toHaveProperty("links"); + expect(response.body.data).not.toHaveProperty("rawHtml"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.markdown).toContain("PagerDuty"); + expect(response.body.data.metadata.statusCode).toBe(200); + expect(response.body.data.metadata.error).toBeUndefined(); + }, + 30000 + ); + + it.concurrent( + "should return a successful response with a valid links on page", + async () => { + const scrapeRequest: ScrapeRequestInput = { + url: "https://roastmywebsite.ai", + formats: ["links"] + }; + + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequest); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + expect(response.body.data).not.toHaveProperty("html"); + expect(response.body.data).not.toHaveProperty("rawHtml"); + expect(response.body.data).toHaveProperty("links"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.links).toContain("https://firecrawl.dev"); + expect(response.body.data.metadata.statusCode).toBe(200); + expect(response.body.data.metadata.error).toBeUndefined(); + }, + 30000 + ); }); -describe("POST /v1/map", () => { - it.concurrent("should require authorization", async () => { - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(401); - }); - - it.concurrent("should return an error response with an invalid API key", async () => { - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer invalid-api-key`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(401); - }); - - it.concurrent("should return a successful response with a valid API key", async () => { - const mapRequest = { - url: "https://roastmywebsite.ai" - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(mapRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("success", true); - expect(response.body).toHaveProperty("links"); - if (!("links" in response.body)) { - throw new Error("Expected response body to have 'links' property"); - } - const links = response.body.links as unknown[]; - expect(Array.isArray(links)).toBe(true); - expect(links.length).toBeGreaterThan(0); - }); - - it.concurrent("should return a successful response with a valid API key and search", async () => { - const mapRequest = { - url: "https://usemotion.com", - search: "pricing" - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(mapRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("success", true); - expect(response.body).toHaveProperty("links"); - if (!("links" in response.body)) { - throw new Error("Expected response body to have 'links' property"); - } - const links = response.body.links as unknown[]; - expect(Array.isArray(links)).toBe(true); - expect(links.length).toBeGreaterThan(0); - expect(links[0]).toContain("usemotion.com/pricing"); - }); - - it.concurrent("should return a successful response with a valid API key and search and allowSubdomains", async () => { - const mapRequest = { - url: "https://firecrawl.dev", - search: "docs", - includeSubdomains: true - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(mapRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("success", true); - expect(response.body).toHaveProperty("links"); - if (!("links" in response.body)) { - throw new Error("Expected response body to have 'links' property"); - } - const links = response.body.links as unknown[]; - expect(Array.isArray(links)).toBe(true); - expect(links.length).toBeGreaterThan(0); - - const containsDocsFirecrawlDev = links.some((link: string) => link.includes("docs.firecrawl.dev")); - expect(containsDocsFirecrawlDev).toBe(true); - }); - - it.concurrent("should return a successful response with a valid API key and search and allowSubdomains and www", async () => { - const mapRequest = { - url: "https://www.firecrawl.dev", - search: "docs", - includeSubdomains: true - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(mapRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("success", true); - expect(response.body).toHaveProperty("links"); - if (!("links" in response.body)) { - throw new Error("Expected response body to have 'links' property"); - } - const links = response.body.links as unknown[]; - expect(Array.isArray(links)).toBe(true); - expect(links.length).toBeGreaterThan(0); - - const containsDocsFirecrawlDev = links.some((link: string) => link.includes("docs.firecrawl.dev")); - expect(containsDocsFirecrawlDev).toBe(true); - }, 10000) - - it.concurrent("should return a successful response with a valid API key and search and not allowSubdomains and www", async () => { - const mapRequest = { - url: "https://www.firecrawl.dev", - search: "docs", - includeSubdomains: false - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(mapRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("success", true); - expect(response.body).toHaveProperty("links"); - if (!("links" in response.body)) { - throw new Error("Expected response body to have 'links' property"); - } - const links = response.body.links as unknown[]; - expect(Array.isArray(links)).toBe(true); - expect(links.length).toBeGreaterThan(0); - expect(links[0]).not.toContain("docs.firecrawl.dev"); - }) - - it.concurrent("should return an error for invalid URL", async () => { - const mapRequest = { - url: "invalid-url", - includeSubdomains: true, - search: "test", - }; - - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/map") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(mapRequest); - - expect(response.statusCode).toBe(400); - expect(response.body).toHaveProperty("success", false); - expect(response.body).toHaveProperty("error"); - }); -}); - - -describe("POST /v1/crawl", () => { - it.concurrent("should require authorization", async () => { - const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/crawl") - .send({ url: "https://firecrawl.dev" }); - expect(response.statusCode).toBe(401); - }); - - it.concurrent("should throw error for blocklisted URL", async () => { - const scrapeRequest: ScrapeRequestInput = { - url: "https://facebook.com/fake-test", - }; - - const response = await request(TEST_URL) - .post("/v1/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(scrapeRequest); - - expect(response.statusCode).toBe(403); - expect(response.body.error).toBe("URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions."); - }); - - it.concurrent( - "should return an error response with an invalid API key", - async () => { + describe("POST /v1/map", () => { + it.concurrent("should require authorization", async () => { const response: ScrapeResponseRequestTest = await request(TEST_URL) - .post("/v1/crawl") - .set("Authorization", `Bearer invalid-api-key`) - .set("Content-Type", "application/json") + .post("/v1/map") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } - ); + }); - it.concurrent("should return a successful response", async () => { - const response = await request(TEST_URL) - .post("/v1/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://firecrawl.dev" }); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("id"); - expect(response.body.id).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") + .set("Authorization", `Bearer invalid-api-key`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + expect(response.statusCode).toBe(401); + } ); - expect(response.body).toHaveProperty("success", true); - expect(response.body).toHaveProperty("url"); - expect(response.body.url).toContain("/v1/crawl/"); - }); - it.concurrent( - "should return a successful response with a valid API key and valid includes option", - async () => { - const crawlResponse = await request(TEST_URL) - .post("/v1/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://firecrawl.dev", - limit: 40, - includePaths: ["blog/*"], - }); + it.concurrent( + "should return a successful response with a valid API key", + async () => { + const mapRequest = { + url: "https://roastmywebsite.ai" + }; - let response; - let isFinished = false; - - while (!isFinished) { - response = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(mapRequest); expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - isFinished = response.body.status === "completed"; - - if (!isFinished) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("links"); + if (!("links" in response.body)) { + throw new Error("Expected response body to have 'links' property"); } + const links = response.body.links as unknown[]; + expect(Array.isArray(links)).toBe(true); + expect(links.length).toBeGreaterThan(0); } + ); - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database - const completedResponse = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + it.concurrent( + "should return a successful response with a valid API key and search", + async () => { + const mapRequest = { + url: "https://usemotion.com", + search: "pricing" + }; - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(5); - urls.forEach((url: string) => { - expect(url).toContain("firecrawl.dev/blog"); - }); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0]).not.toHaveProperty("content"); // v0 - expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); - }, - 180000 - ); // 180 seconds - - it.concurrent( - "should return a successful response with a valid API key and valid excludes option", - async () => { - const crawlResponse = await request(TEST_URL) - .post("/v1/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ - url: "https://firecrawl.dev", - limit: 40, - excludePaths: ["blog/*"], - }); - - let isFinished = false; - let response; - - while (!isFinished) { - response = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(mapRequest); expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - isFinished = response.body.status === "completed"; - - if (!isFinished) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("links"); + if (!("links" in response.body)) { + throw new Error("Expected response body to have 'links' property"); } + const links = response.body.links as unknown[]; + expect(Array.isArray(links)).toBe(true); + expect(links.length).toBeGreaterThan(0); + expect(links[0]).toContain("usemotion.com/pricing"); } + ); - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database - const completedResponse = await request( - TEST_URL - ) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + it.concurrent( + "should return a successful response with a valid API key and search and allowSubdomains", + async () => { + const mapRequest = { + url: "https://firecrawl.dev", + search: "docs", + includeSubdomains: true + }; - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(3); - urls.forEach((url: string) => { - expect(url.startsWith("https://www.firecrawl.dev/blog/")).toBeFalsy(); - }); - }, - 90000 - ); // 90 seconds + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(mapRequest); - it.concurrent( - "should return a successful response with max depth option for a valid crawl job", - async () => { - const crawlResponse = await request(TEST_URL) - .post("/v1/crawl") + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("links"); + if (!("links" in response.body)) { + throw new Error("Expected response body to have 'links' property"); + } + const links = response.body.links as unknown[]; + expect(Array.isArray(links)).toBe(true); + expect(links.length).toBeGreaterThan(0); + + const containsDocsFirecrawlDev = links.some((link: string) => + link.includes("docs.firecrawl.dev") + ); + expect(containsDocsFirecrawlDev).toBe(true); + } + ); + + it.concurrent( + "should return a successful response with a valid API key and search and allowSubdomains and www", + async () => { + const mapRequest = { + url: "https://www.firecrawl.dev", + search: "docs", + includeSubdomains: true + }; + + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(mapRequest); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("links"); + if (!("links" in response.body)) { + throw new Error("Expected response body to have 'links' property"); + } + const links = response.body.links as unknown[]; + expect(Array.isArray(links)).toBe(true); + expect(links.length).toBeGreaterThan(0); + + const containsDocsFirecrawlDev = links.some((link: string) => + link.includes("docs.firecrawl.dev") + ); + expect(containsDocsFirecrawlDev).toBe(true); + }, + 10000 + ); + + it.concurrent( + "should return a successful response with a valid API key and search and not allowSubdomains and www", + async () => { + const mapRequest = { + url: "https://www.firecrawl.dev", + search: "docs", + includeSubdomains: false + }; + + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(mapRequest); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("links"); + if (!("links" in response.body)) { + throw new Error("Expected response body to have 'links' property"); + } + const links = response.body.links as unknown[]; + expect(Array.isArray(links)).toBe(true); + expect(links.length).toBeGreaterThan(0); + expect(links[0]).not.toContain("docs.firecrawl.dev"); + } + ); + + it.concurrent("should return an error for invalid URL", async () => { + const mapRequest = { + url: "invalid-url", + includeSubdomains: true, + search: "test" + }; + + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/map") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") - .send({ - url: "https://www.scrapethissite.com", - maxDepth: 1, - }); - expect(crawlResponse.statusCode).toBe(200); + .send(mapRequest); - const response = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("status"); - expect(["active", "waiting", "completed", "scraping"]).toContain(response.body.status); - // wait for 60 seconds - let isCompleted = false; - while (!isCompleted) { - const statusCheckResponse = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(statusCheckResponse.statusCode).toBe(200); - isCompleted = statusCheckResponse.body.status === "completed"; - if (!isCompleted) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again - } - } - const completedResponse = await request( - TEST_URL - ) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).not.toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); - const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL - ); - expect(urls.length).toBeGreaterThan(1); - - // Check if all URLs have a maximum depth of 1 - urls.forEach((url: string) => { - const pathSplits = new URL(url).pathname.split("/"); - const depth = - pathSplits.length - - (pathSplits[0].length === 0 && - pathSplits[pathSplits.length - 1].length === 0 - ? 1 - : 0); - expect(depth).toBeLessThanOrEqual(2); - }); - }, - 180000 - ); -}) - -describe("GET /v1/crawl/:jobId", () => { - it.concurrent("should require authorization", async () => { - const response = await request(TEST_URL).get("/v1/crawl/123"); - expect(response.statusCode).toBe(401); + expect(response.statusCode).toBe(400); + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error"); + }); }); - it.concurrent( - "should return an error response with an invalid API key", - async () => { - const response = await request(TEST_URL) - .get("/v1/crawl/123") - .set("Authorization", `Bearer invalid-api-key`); + describe("POST /v1/crawl", () => { + it.concurrent("should require authorization", async () => { + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/crawl") + .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } - ); + }); + + it.concurrent("should throw error for blocklisted URL", async () => { + const scrapeRequest: ScrapeRequestInput = { + url: "https://facebook.com/fake-test" + }; - it.concurrent( - "should return Job not found for invalid job ID", - async () => { const response = await request(TEST_URL) - .get("/v1/crawl/invalidJobId") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(response.statusCode).toBe(404); - } - ); - - it.concurrent( - "should return a successful crawl status response for a valid crawl job", - async () => { - const crawlResponse = await request(TEST_URL) .post("/v1/crawl") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") - .send({ url: "https://docs.firecrawl.dev" }); - expect(crawlResponse.statusCode).toBe(200); + .send(scrapeRequest); - let isCompleted = false; + expect(response.statusCode).toBe(403); + expect(response.body.error).toBe( + "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions." + ); + }); + + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response: ScrapeResponseRequestTest = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer invalid-api-key`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + expect(response.statusCode).toBe(401); + } + ); + + it.concurrent("should return a successful response", async () => { + const response = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://firecrawl.dev" }); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("id"); + expect(response.body.id).toMatch( + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + ); + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("url"); + expect(response.body.url).toContain("/v1/crawl/"); + }); + + it.concurrent( + "should return a successful response with a valid API key and valid includes option", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://firecrawl.dev", + limit: 40, + includePaths: ["blog/*"] + }); + + let response; + let isFinished = false; + + while (!isFinished) { + response = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + isFinished = response.body.status === "completed"; + + if (!isFinished) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database + const completedResponse = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(5); + urls.forEach((url: string) => { + expect(url).toContain("firecrawl.dev/blog"); + }); + + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0]).not.toHaveProperty("content"); // v0 + expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); + expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); + }, + 180000 + ); // 180 seconds + + it.concurrent( + "should return a successful response with a valid API key and valid excludes option", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://firecrawl.dev", + limit: 40, + excludePaths: ["blog/*"] + }); + + let isFinished = false; + let response; + + while (!isFinished) { + response = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); + isFinished = response.body.status === "completed"; + + if (!isFinished) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database + const completedResponse = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(3); + urls.forEach((url: string) => { + expect(url.startsWith("https://www.firecrawl.dev/blog/")).toBeFalsy(); + }); + }, + 90000 + ); // 90 seconds + + it.concurrent( + "should return a successful response with max depth option for a valid crawl job", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ + url: "https://www.scrapethissite.com", + maxDepth: 1 + }); + expect(crawlResponse.statusCode).toBe(200); - while (!isCompleted) { const response = await request(TEST_URL) .get(`/v1/crawl/${crawlResponse.body.id}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("status"); - - if (response.body.status === "completed") { - isCompleted = true; - } else { - await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again + expect(["active", "waiting", "completed", "scraping"]).toContain( + response.body.status + ); + // wait for 60 seconds + let isCompleted = false; + while (!isCompleted) { + const statusCheckResponse = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(statusCheckResponse.statusCode).toBe(200); + isCompleted = statusCheckResponse.body.status === "completed"; + if (!isCompleted) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before checking again + } } + const completedResponse = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).not.toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); + expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); + const urls = completedResponse.body.data.map( + (item: any) => item.metadata?.sourceURL + ); + expect(urls.length).toBeGreaterThan(1); + + // Check if all URLs have a maximum depth of 1 + urls.forEach((url: string) => { + const pathSplits = new URL(url).pathname.split("/"); + const depth = + pathSplits.length - + (pathSplits[0].length === 0 && + pathSplits[pathSplits.length - 1].length === 0 + ? 1 + : 0); + expect(depth).toBeLessThanOrEqual(2); + }); + }, + 180000 + ); + }); + + describe("GET /v1/crawl/:jobId", () => { + it.concurrent("should require authorization", async () => { + const response = await request(TEST_URL).get("/v1/crawl/123"); + expect(response.statusCode).toBe(401); + }); + + it.concurrent( + "should return an error response with an invalid API key", + async () => { + const response = await request(TEST_URL) + .get("/v1/crawl/123") + .set("Authorization", `Bearer invalid-api-key`); + expect(response.statusCode).toBe(401); } + ); - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database - const completedResponse = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + it.concurrent( + "should return Job not found for invalid job ID", + async () => { + const response = await request(TEST_URL) + .get("/v1/crawl/invalidJobId") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(response.statusCode).toBe(404); + } + ); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("completed"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).not.toHaveProperty("content"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); - expect( - completedResponse.body.data[0].metadata.error - ).toBeUndefined(); + it.concurrent( + "should return a successful crawl status response for a valid crawl job", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://docs.firecrawl.dev" }); + expect(crawlResponse.statusCode).toBe(200); - const childrenLinks = completedResponse.body.data.filter( - (doc) => - doc.metadata && - doc.metadata.sourceURL - ); + let isCompleted = false; - expect(childrenLinks.length).toBe(completedResponse.body.data.length); - }, - 180000 - ); // 120 seconds + while (!isCompleted) { + const response = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("status"); - it.concurrent( - "If someone cancels a crawl job, it should turn into failed status", - async () => { - const crawlResponse = await request(TEST_URL) - .post("/v1/crawl") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send({ url: "https://docs.firecrawl.dev", limit: 10 }); + if (response.body.status === "completed") { + isCompleted = true; + } else { + await new Promise((r) => setTimeout(r, 1000)); // Wait for 1 second before checking again + } + } - expect(crawlResponse.statusCode).toBe(200); + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database + const completedResponse = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - await new Promise((r) => setTimeout(r, 10000)); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("completed"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).not.toHaveProperty("content"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); + expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); - const responseCancel = await request(TEST_URL) - .delete(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - expect(responseCancel.statusCode).toBe(200); - expect(responseCancel.body).toHaveProperty("status"); - expect(responseCancel.body.status).toBe("cancelled"); + const childrenLinks = completedResponse.body.data.filter( + (doc) => doc.metadata && doc.metadata.sourceURL + ); - await new Promise((r) => setTimeout(r, 10000)); - const completedResponse = await request(TEST_URL) - .get(`/v1/crawl/${crawlResponse.body.id}`) - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); - - expect(completedResponse.statusCode).toBe(200); - expect(completedResponse.body).toHaveProperty("status"); - expect(completedResponse.body.status).toBe("cancelled"); - expect(completedResponse.body).toHaveProperty("data"); - expect(completedResponse.body.data[0]).toHaveProperty("markdown"); - expect(completedResponse.body.data[0]).toHaveProperty("metadata"); - expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); - expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); - }, - 60000 - ); // 60 seconds -}) + expect(childrenLinks.length).toBe(completedResponse.body.data.length); + }, + 180000 + ); // 120 seconds + + it.concurrent( + "If someone cancels a crawl job, it should turn into failed status", + async () => { + const crawlResponse = await request(TEST_URL) + .post("/v1/crawl") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://docs.firecrawl.dev", limit: 10 }); + + expect(crawlResponse.statusCode).toBe(200); + + await new Promise((r) => setTimeout(r, 10000)); + + const responseCancel = await request(TEST_URL) + .delete(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + expect(responseCancel.statusCode).toBe(200); + expect(responseCancel.body).toHaveProperty("status"); + expect(responseCancel.body.status).toBe("cancelled"); + + await new Promise((r) => setTimeout(r, 10000)); + const completedResponse = await request(TEST_URL) + .get(`/v1/crawl/${crawlResponse.body.id}`) + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); + + expect(completedResponse.statusCode).toBe(200); + expect(completedResponse.body).toHaveProperty("status"); + expect(completedResponse.body.status).toBe("cancelled"); + expect(completedResponse.body).toHaveProperty("data"); + expect(completedResponse.body.data[0]).toHaveProperty("markdown"); + expect(completedResponse.body.data[0]).toHaveProperty("metadata"); + expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); + expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); + }, + 60000 + ); // 60 seconds + }); }); diff --git a/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts b/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts index 5c7feb1f..e297f7c8 100644 --- a/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts +++ b/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts @@ -2,7 +2,7 @@ import request from "supertest"; import { configDotenv } from "dotenv"; import { ScrapeRequest, - ScrapeResponseRequestTest, + ScrapeResponseRequestTest } from "../../controllers/v1/types"; configDotenv(); @@ -10,31 +10,39 @@ const FIRECRAWL_API_URL = "http://127.0.0.1:3002"; const E2E_TEST_SERVER_URL = "http://firecrawl-e2e-test.vercel.app"; // @rafaelsideguide/firecrawl-e2e-test describe("E2E Tests for v1 API Routes", () => { + it.concurrent( + "should return a successful response for a scrape with 403 page", + async () => { + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send({ url: "https://httpstat.us/403" }); - it.concurrent('should return a successful response for a scrape with 403 page', async () => { - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) - .post('/v1/scrape') - .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`) - .set('Content-Type', 'application/json') - .send({ url: 'https://httpstat.us/403' }); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + expect(response.body.data).toHaveProperty("markdown"); + expect(response.body.data).toHaveProperty("metadata"); + expect(response.body.data.metadata.statusCode).toBe(403); + }, + 30000 + ); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - expect(response.body.data).toHaveProperty('markdown'); - expect(response.body.data).toHaveProperty('metadata'); - expect(response.body.data.metadata.statusCode).toBe(403); - }, 30000); - - it.concurrent("should handle 'formats:markdown (default)' parameter correctly", + it.concurrent( + "should handle 'formats:markdown (default)' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL } as ScrapeRequest; - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") @@ -47,27 +55,41 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data).toHaveProperty("markdown"); - - expect(response.body.data.markdown).toContain("This page is used for end-to-end (e2e) testing with Firecrawl."); - expect(response.body.data.markdown).toContain("Content with id #content-1"); + + expect(response.body.data.markdown).toContain( + "This page is used for end-to-end (e2e) testing with Firecrawl." + ); + expect(response.body.data.markdown).toContain( + "Content with id #content-1" + ); // expect(response.body.data.markdown).toContain("Loading..."); expect(response.body.data.markdown).toContain("Click me!"); - expect(response.body.data.markdown).toContain("Power your AI apps with clean data crawled from any website. It's also open-source."); // firecrawl.dev inside an iframe - expect(response.body.data.markdown).toContain("This content loads only when you see it. Don't blink! 👼"); // the browser always scroll to the bottom + expect(response.body.data.markdown).toContain( + "Power your AI apps with clean data crawled from any website. It's also open-source." + ); // firecrawl.dev inside an iframe + expect(response.body.data.markdown).toContain( + "This content loads only when you see it. Don't blink! 👼" + ); // the browser always scroll to the bottom expect(response.body.data.markdown).not.toContain("Header"); // Only main content is returned by default expect(response.body.data.markdown).not.toContain("footer"); // Only main content is returned by default - expect(response.body.data.markdown).not.toContain("This content is only visible on mobile"); + expect(response.body.data.markdown).not.toContain( + "This content is only visible on mobile" + ); }, - 30000); + 30000 + ); - it.concurrent("should handle 'formats:html' parameter correctly", + it.concurrent( + "should handle 'formats:html' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, formats: ["html"] } as ScrapeRequest; - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") @@ -79,23 +101,30 @@ describe("E2E Tests for v1 API Routes", () => { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data).not.toHaveProperty("markdown"); expect(response.body.data).toHaveProperty("html"); - expect(response.body.data.html).not.toContain("

Header
"); - expect(response.body.data.html).toContain("

This page is used for end-to-end (e2e) testing with Firecrawl.

"); + expect(response.body.data.html).not.toContain( + '
Header
' + ); + expect(response.body.data.html).toContain( + '

This page is used for end-to-end (e2e) testing with Firecrawl.

' + ); }, - 30000); + 30000 + ); - it.concurrent("should handle 'rawHtml' in 'formats' parameter correctly", + it.concurrent( + "should handle 'rawHtml' in 'formats' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, formats: ["rawHtml"] } as ScrapeRequest; - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") @@ -110,45 +139,30 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).not.toHaveProperty("markdown"); expect(response.body.data).toHaveProperty("rawHtml"); - expect(response.body.data.rawHtml).toContain(">This page is used for end-to-end (e2e) testing with Firecrawl.

"); + expect(response.body.data.rawHtml).toContain( + ">This page is used for end-to-end (e2e) testing with Firecrawl.

" + ); expect(response.body.data.rawHtml).toContain(">Header"); }, - 30000); - + 30000 + ); + // - TODO: tests for links // - TODO: tests for screenshot // - TODO: tests for screenshot@fullPage - it.concurrent("should handle 'headers' parameter correctly", async () => { - // @ts-ignore - const scrapeRequest = { - url: E2E_TEST_SERVER_URL, - headers: { "e2e-header-test": "firecrawl" } - } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) - .post("/v1/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(scrapeRequest); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty("data"); - if (!("data" in response.body)) { - throw new Error("Expected response body to have 'data' property"); - } - - expect(response.body.data.markdown).toContain("e2e-header-test: firecrawl"); - }, 30000); - - it.concurrent("should handle 'includeTags' parameter correctly", + it.concurrent( + "should handle 'headers' parameter correctly", async () => { + // @ts-ignore const scrapeRequest = { url: E2E_TEST_SERVER_URL, - includeTags: ['#content-1'] + headers: { "e2e-header-test": "firecrawl" } } as ScrapeRequest; - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") @@ -160,73 +174,126 @@ describe("E2E Tests for v1 API Routes", () => { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data.markdown).not.toContain("

This page is used for end-to-end (e2e) testing with Firecrawl.

"); - expect(response.body.data.markdown).toContain("Content with id #content-1"); + expect(response.body.data.markdown).toContain( + "e2e-header-test: firecrawl" + ); }, - 30000); - - it.concurrent("should handle 'excludeTags' parameter correctly", + 30000 + ); + + it.concurrent( + "should handle 'includeTags' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - excludeTags: ['#content-1'] + includeTags: ["#content-1"] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data.markdown).toContain("This page is used for end-to-end (e2e) testing with Firecrawl."); - expect(response.body.data.markdown).not.toContain("Content with id #content-1"); + expect(response.body.data.markdown).not.toContain( + "

This page is used for end-to-end (e2e) testing with Firecrawl.

" + ); + expect(response.body.data.markdown).toContain( + "Content with id #content-1" + ); }, - 30000); - - it.concurrent("should handle 'onlyMainContent' parameter correctly", + 30000 + ); + + it.concurrent( + "should handle 'excludeTags' parameter correctly", + async () => { + const scrapeRequest = { + url: E2E_TEST_SERVER_URL, + excludeTags: ["#content-1"] + } as ScrapeRequest; + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequest); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty("data"); + if (!("data" in response.body)) { + throw new Error("Expected response body to have 'data' property"); + } + + expect(response.body.data.markdown).toContain( + "This page is used for end-to-end (e2e) testing with Firecrawl." + ); + expect(response.body.data.markdown).not.toContain( + "Content with id #content-1" + ); + }, + 30000 + ); + + it.concurrent( + "should handle 'onlyMainContent' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, formats: ["html", "markdown"], onlyMainContent: false } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - - expect(response.body.data.markdown).toContain("This page is used for end-to-end (e2e) testing with Firecrawl."); - expect(response.body.data.html).toContain("
Header
"); + + expect(response.body.data.markdown).toContain( + "This page is used for end-to-end (e2e) testing with Firecrawl." + ); + expect(response.body.data.html).toContain( + '
Header
' + ); }, - 30000); - - it.concurrent("should handle 'timeout' parameter correctly", + 30000 + ); + + it.concurrent( + "should handle 'timeout' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, timeout: 500 } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(408); if (!("error" in response.body)) { @@ -234,65 +301,87 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.error).toBe("Request timed out"); expect(response.body.success).toBe(false); - }, 30000); + }, + 30000 + ); - - it.concurrent("should handle 'mobile' parameter correctly", + it.concurrent( + "should handle 'mobile' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, mobile: true } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data.markdown).toContain("This content is only visible on mobile"); + expect(response.body.data.markdown).toContain( + "This content is only visible on mobile" + ); }, - 30000); - - it.concurrent("should handle 'parsePDF' parameter correctly", - async () => { - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + 30000 + ); + + it.concurrent( + "should handle 'parsePDF' parameter correctly", + async () => { + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") - .send({ url: 'https://arxiv.org/pdf/astro-ph/9301001.pdf'}); + .send({ url: "https://arxiv.org/pdf/astro-ph/9301001.pdf" }); await new Promise((r) => setTimeout(r, 6000)); expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); + expect(response.body).toHaveProperty("data"); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(response.body.data.markdown).toContain('arXiv:astro-ph/9301001v1 7 Jan 1993'); - expect(response.body.data.markdown).not.toContain('h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm'); + expect(response.body.data.markdown).toContain( + "arXiv:astro-ph/9301001v1 7 Jan 1993" + ); + expect(response.body.data.markdown).not.toContain( + "h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm" + ); - const responseNoParsePDF: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + const responseNoParsePDF: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") - .send({ url: 'https://arxiv.org/pdf/astro-ph/9301001.pdf', parsePDF: false }); + .send({ + url: "https://arxiv.org/pdf/astro-ph/9301001.pdf", + parsePDF: false + }); await new Promise((r) => setTimeout(r, 6000)); expect(responseNoParsePDF.statusCode).toBe(200); - expect(responseNoParsePDF.body).toHaveProperty('data'); + expect(responseNoParsePDF.body).toHaveProperty("data"); if (!("data" in responseNoParsePDF.body)) { throw new Error("Expected response body to have 'data' property"); } - expect(responseNoParsePDF.body.data.markdown).toContain('h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm'); + expect(responseNoParsePDF.body.data.markdown).toContain( + "h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm" + ); }, - 30000); - + 30000 + ); + // it.concurrent("should handle 'location' parameter correctly", // async () => { // const scrapeRequest: ScrapeRequest = { @@ -302,76 +391,85 @@ describe("E2E Tests for v1 API Routes", () => { // languages: ["en"] // } // }; - + // const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) // .post("/v1/scrape") // .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) // .set("Content-Type", "application/json") // .send(scrapeRequest); - + // expect(response.statusCode).toBe(200); // // Add assertions to verify location is handled correctly // }, // 30000); - - it.concurrent("should handle 'skipTlsVerification' parameter correctly", + + it.concurrent( + "should handle 'skipTlsVerification' parameter correctly", async () => { const scrapeRequest = { url: "https://expired.badssl.com/", timeout: 120000 } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - console.log("Error1a") - // console.log(response.body) + console.log("Error1a"); + // console.log(response.body) expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } expect(response.body.data.metadata.pageStatusCode).toBe(500); - console.log("Error?") - + console.log("Error?"); + const scrapeRequestWithSkipTlsVerification = { url: "https://expired.badssl.com/", skipTlsVerification: true, timeout: 120000 - } as ScrapeRequest; - - const responseWithSkipTlsVerification: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) - .post("/v1/scrape") - .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) - .set("Content-Type", "application/json") - .send(scrapeRequestWithSkipTlsVerification); - - console.log("Error1b") + + const responseWithSkipTlsVerification: ScrapeResponseRequestTest = + await request(FIRECRAWL_API_URL) + .post("/v1/scrape") + .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) + .set("Content-Type", "application/json") + .send(scrapeRequestWithSkipTlsVerification); + + console.log("Error1b"); // console.log(responseWithSkipTlsVerification.body) expect(responseWithSkipTlsVerification.statusCode).toBe(200); if (!("data" in responseWithSkipTlsVerification.body)) { throw new Error("Expected response body to have 'data' property"); } // console.log(responseWithSkipTlsVerification.body.data) - expect(responseWithSkipTlsVerification.body.data.markdown).toContain("badssl.com"); + expect(responseWithSkipTlsVerification.body.data.markdown).toContain( + "badssl.com" + ); }, - 60000); - - it.concurrent("should handle 'removeBase64Images' parameter correctly", + 60000 + ); + + it.concurrent( + "should handle 'removeBase64Images' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, removeBase64Images: true } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); @@ -380,49 +478,63 @@ describe("E2E Tests for v1 API Routes", () => { // - TODO: not working for every image // expect(response.body.data.markdown).toContain("Image-Removed"); }, - 30000); + 30000 + ); - it.concurrent("should handle 'action wait' parameter correctly", + it.concurrent( + "should handle 'action wait' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - actions: [{ - type: "wait", - milliseconds: 10000 - }] + actions: [ + { + type: "wait", + milliseconds: 10000 + } + ] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } expect(response.body.data.markdown).not.toContain("Loading..."); - expect(response.body.data.markdown).toContain("Content loaded after 5 seconds!"); + expect(response.body.data.markdown).toContain( + "Content loaded after 5 seconds!" + ); }, - 30000); + 30000 + ); // screenshot - it.concurrent("should handle 'action screenshot' parameter correctly", + it.concurrent( + "should handle 'action screenshot' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - actions: [{ - type: "screenshot" - }] + actions: [ + { + type: "screenshot" + } + ] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); @@ -430,32 +542,42 @@ describe("E2E Tests for v1 API Routes", () => { if (!response.body.data.actions?.screenshots) { throw new Error("Expected response body to have screenshots array"); } - expect(response.body.data.actions.screenshots[0].length).toBeGreaterThan(0); - expect(response.body.data.actions.screenshots[0]).toContain("https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-"); + expect(response.body.data.actions.screenshots[0].length).toBeGreaterThan( + 0 + ); + expect(response.body.data.actions.screenshots[0]).toContain( + "https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-" + ); // TODO compare screenshot with expected screenshot }, - 30000); + 30000 + ); - it.concurrent("should handle 'action screenshot@fullPage' parameter correctly", + it.concurrent( + "should handle 'action screenshot@fullPage' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - actions: [{ - type: "screenshot", - fullPage: true - }, - { - type:"scrape" - }] + actions: [ + { + type: "screenshot", + fullPage: true + }, + { + type: "scrape" + } + ] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); @@ -464,77 +586,101 @@ describe("E2E Tests for v1 API Routes", () => { if (!response.body.data.actions?.screenshots) { throw new Error("Expected response body to have screenshots array"); } - expect(response.body.data.actions.screenshots[0].length).toBeGreaterThan(0); - expect(response.body.data.actions.screenshots[0]).toContain("https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-"); + expect(response.body.data.actions.screenshots[0].length).toBeGreaterThan( + 0 + ); + expect(response.body.data.actions.screenshots[0]).toContain( + "https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-" + ); if (!response.body.data.actions?.scrapes) { - throw new Error("Expected response body to have scrapes array"); + throw new Error("Expected response body to have scrapes array"); } - expect(response.body.data.actions.scrapes[0].url).toBe("https://firecrawl-e2e-test.vercel.app/"); - expect(response.body.data.actions.scrapes[0].html).toContain("This page is used for end-to-end (e2e) testing with Firecrawl.

"); + expect(response.body.data.actions.scrapes[0].url).toBe( + "https://firecrawl-e2e-test.vercel.app/" + ); + expect(response.body.data.actions.scrapes[0].html).toContain( + "This page is used for end-to-end (e2e) testing with Firecrawl.

" + ); // TODO compare screenshot with expected full page screenshot }, - 30000); + 30000 + ); - it.concurrent("should handle 'action click' parameter correctly", + it.concurrent( + "should handle 'action click' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - actions: [{ - type: "click", - selector: "#click-me" - }] + actions: [ + { + type: "click", + selector: "#click-me" + } + ] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } expect(response.body.data.markdown).not.toContain("Click me!"); - expect(response.body.data.markdown).toContain("Text changed after click!"); + expect(response.body.data.markdown).toContain( + "Text changed after click!" + ); }, - 30000); + 30000 + ); - it.concurrent("should handle 'action write' parameter correctly", + it.concurrent( + "should handle 'action write' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, formats: ["html"], - actions: [{ - type: "click", - selector: "#input-1" - }, - { - type: "write", - text: "Hello, world!" - } - ]} as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + actions: [ + { + type: "click", + selector: "#input-1" + }, + { + type: "write", + text: "Hello, world!" + } + ] + } as ScrapeRequest; + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + expect(response.statusCode).toBe(200); if (!("data" in response.body)) { throw new Error("Expected response body to have 'data' property"); } - + // TODO: fix this test (need to fix fire-engine first) // uncomment the following line: // expect(response.body.data.html).toContain(""); }, - 30000); + 30000 + ); // TODO: fix this test (need to fix fire-engine first) - it.concurrent("should handle 'action pressKey' parameter correctly", + it.concurrent( + "should handle 'action pressKey' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, @@ -546,13 +692,15 @@ describe("E2E Tests for v1 API Routes", () => { } ] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + // // TODO: fix this test (need to fix fire-engine first) // // right now response.body is: { success: false, error: '(Internal server error) - null' } // expect(response.statusCode).toBe(200); @@ -561,10 +709,12 @@ describe("E2E Tests for v1 API Routes", () => { // } // expect(response.body.data.markdown).toContain("Last Key Clicked: ArrowDown") }, - 30000); + 30000 + ); // TODO: fix this test (need to fix fire-engine first) - it.concurrent("should handle 'action scroll' parameter correctly", + it.concurrent( + "should handle 'action scroll' parameter correctly", async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, @@ -581,23 +731,25 @@ describe("E2E Tests for v1 API Routes", () => { } ] } as ScrapeRequest; - - const response: ScrapeResponseRequestTest = await request(FIRECRAWL_API_URL) + + const response: ScrapeResponseRequestTest = await request( + FIRECRAWL_API_URL + ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send(scrapeRequest); - + // TODO: uncomment this tests // expect(response.statusCode).toBe(200); // if (!("data" in response.body)) { // throw new Error("Expected response body to have 'data' property"); // } - // + // // expect(response.body.data.markdown).toContain("You have reached the bottom!") }, - 30000); + 30000 + ); // TODO: test scrape action - -}); \ No newline at end of file +}); diff --git a/apps/api/src/__tests__/e2e_withAuth/index.test.ts b/apps/api/src/__tests__/e2e_withAuth/index.test.ts index 90a4587d..e026eef0 100644 --- a/apps/api/src/__tests__/e2e_withAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_withAuth/index.test.ts @@ -3,7 +3,7 @@ import dotenv from "dotenv"; import { FirecrawlCrawlResponse, FirecrawlCrawlStatusResponse, - FirecrawlScrapeResponse, + FirecrawlScrapeResponse } from "../../types"; dotenv.config(); @@ -28,9 +28,8 @@ describe("E2E Tests for v0 API Routes", () => { describe("POST /v0/scrape", () => { it.concurrent("should require authorization", async () => { - const response: FirecrawlScrapeResponse = await request(TEST_URL).post( - "/v0/scrape" - ); + const response: FirecrawlScrapeResponse = + await request(TEST_URL).post("/v0/scrape"); expect(response.statusCode).toBe(401); }); @@ -99,7 +98,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true }, + pageOptions: { includeHtml: true } }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -196,7 +195,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com/", - pageOptions: { removeTags: [".nav", "#footer", "strong"] }, + pageOptions: { removeTags: [".nav", "#footer", "strong"] } }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -338,9 +337,8 @@ describe("E2E Tests for v0 API Routes", () => { describe("POST /v0/crawl", () => { it.concurrent("should require authorization", async () => { - const response: FirecrawlCrawlResponse = await request(TEST_URL).post( - "/v0/crawl" - ); + const response: FirecrawlCrawlResponse = + await request(TEST_URL).post("/v0/crawl"); expect(response.statusCode).toBe(401); }); @@ -383,8 +381,8 @@ describe("E2E Tests for v0 API Routes", () => { url: "https://mendable.ai", limit: 10, crawlerOptions: { - includes: ["blog/*"], - }, + includes: ["blog/*"] + } }); let response: FirecrawlCrawlStatusResponse; @@ -446,8 +444,8 @@ describe("E2E Tests for v0 API Routes", () => { url: "https://mendable.ai", limit: 10, crawlerOptions: { - excludes: ["blog/*"], - }, + excludes: ["blog/*"] + } }); let isFinished = false; @@ -494,7 +492,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com", - crawlerOptions: { maxDepth: 1 }, + crawlerOptions: { maxDepth: 1 } }); expect(crawlResponse.statusCode).toBe(200); @@ -690,7 +688,9 @@ describe("E2E Tests for v0 API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].content).toContain("Firecrawl"); - expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe(200); + expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( + 200 + ); expect( completedResponse.body.data[0].metadata.pageError ).toBeUndefined(); @@ -760,7 +760,10 @@ describe("E2E Tests for v0 API Routes", () => { .post("/v0/crawl") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") - .send({ url: "https://docs.tatum.io", crawlerOptions: { limit: 200 } }); + .send({ + url: "https://docs.tatum.io", + crawlerOptions: { limit: 200 } + }); expect(crawlResponse.statusCode).toBe(200); @@ -825,7 +828,7 @@ describe("E2E Tests for v0 API Routes", () => { .send({ url: "https://mendable.ai", pageOptions: { - onlyMainContent: true, + onlyMainContent: true }, extractorOptions: { mode: "llm-extraction", @@ -835,18 +838,18 @@ describe("E2E Tests for v0 API Routes", () => { type: "object", properties: { company_mission: { - type: "string", + type: "string" }, supports_sso: { - type: "boolean", + type: "boolean" }, is_open_source: { - type: "boolean", - }, + type: "boolean" + } }, - required: ["company_mission", "supports_sso", "is_open_source"], - }, - }, + required: ["company_mission", "supports_sso", "is_open_source"] + } + } }); // Ensure that the job was successfully created before proceeding with LLM extraction diff --git a/apps/api/src/controllers/__tests__/crawl.test.ts b/apps/api/src/controllers/__tests__/crawl.test.ts index e65523cb..81fa2e5d 100644 --- a/apps/api/src/controllers/__tests__/crawl.test.ts +++ b/apps/api/src/controllers/__tests__/crawl.test.ts @@ -1,30 +1,30 @@ -import { crawlController } from '../v0/crawl' -import { Request, Response } from 'express'; -import { authenticateUser } from '../auth'; // Ensure this import is correct -import { createIdempotencyKey } from '../../services/idempotency/create'; -import { validateIdempotencyKey } from '../../services/idempotency/validate'; -import { v4 as uuidv4 } from 'uuid'; +import { crawlController } from "../v0/crawl"; +import { Request, Response } from "express"; +import { authenticateUser } from "../auth"; // Ensure this import is correct +import { createIdempotencyKey } from "../../services/idempotency/create"; +import { validateIdempotencyKey } from "../../services/idempotency/validate"; +import { v4 as uuidv4 } from "uuid"; -jest.mock('../auth', () => ({ +jest.mock("../auth", () => ({ authenticateUser: jest.fn().mockResolvedValue({ success: true, - team_id: 'team123', + team_id: "team123", error: null, status: 200 }), reduce: jest.fn() })); -jest.mock('../../services/idempotency/validate'); +jest.mock("../../services/idempotency/validate"); -describe('crawlController', () => { - it('should prevent duplicate requests using the same idempotency key', async () => { +describe("crawlController", () => { + it("should prevent duplicate requests using the same idempotency key", async () => { const req = { headers: { - 'x-idempotency-key': await uuidv4(), - 'Authorization': `Bearer ${process.env.TEST_API_KEY}` + "x-idempotency-key": await uuidv4(), + Authorization: `Bearer ${process.env.TEST_API_KEY}` }, body: { - url: 'https://mendable.ai' + url: "https://mendable.ai" } } as unknown as Request; const res = { @@ -33,7 +33,9 @@ describe('crawlController', () => { } as unknown as Response; // Mock the idempotency key validation to return false for the second call - (validateIdempotencyKey as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(false); + (validateIdempotencyKey as jest.Mock) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); // First request should succeed await crawlController(req, res); @@ -42,6 +44,8 @@ describe('crawlController', () => { // Second request with the same key should fail await crawlController(req, res); expect(res.status).toHaveBeenCalledWith(409); - expect(res.json).toHaveBeenCalledWith({ error: 'Idempotency key already used' }); + expect(res.json).toHaveBeenCalledWith({ + error: "Idempotency key already used" + }); }); -}); \ No newline at end of file +}); diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index 8f4d49ea..947c2784 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -4,7 +4,7 @@ import { AuthResponse, NotificationType, PlanType, - RateLimiterMode, + RateLimiterMode } from "../types"; import { supabase_service } from "../services/supabase"; import { withAuth } from "../lib/withAuth"; @@ -39,7 +39,8 @@ function normalizedApiIsUuid(potentialUuid: string): boolean { export async function setCachedACUC( api_key: string, acuc: - | AuthCreditUsageChunk | null + | AuthCreditUsageChunk + | null | ((acuc: AuthCreditUsageChunk) => AuthCreditUsageChunk | null) ) { const cacheKeyACUC = `acuc_${api_key}`; @@ -48,7 +49,7 @@ export async function setCachedACUC( try { await redlock.using([redLockKey], 10000, {}, async (signal) => { if (typeof acuc === "function") { - acuc = acuc(JSON.parse(await getValue(cacheKeyACUC) ?? "null")); + acuc = acuc(JSON.parse((await getValue(cacheKeyACUC)) ?? "null")); if (acuc === null) { if (signal.aborted) { @@ -125,7 +126,7 @@ export async function getACUC( if (chunk !== null && useCache) { setCachedACUC(api_key, chunk); } - + // console.log(chunk); return chunk; @@ -134,9 +135,7 @@ export async function getACUC( } } -export async function clearACUC( - api_key: string, -): Promise { +export async function clearACUC(api_key: string): Promise { const cacheKeyACUC = `acuc_${api_key}`; await deleteKey(cacheKeyACUC); } @@ -146,7 +145,11 @@ export async function authenticateUser( res, mode?: RateLimiterMode ): Promise { - return withAuth(supaAuthenticateUser, { success: true, chunk: null, team_id: "bypass" })(req, res, mode); + return withAuth(supaAuthenticateUser, { + success: true, + chunk: null, + team_id: "bypass" + })(req, res, mode); } export async function supaAuthenticateUser( @@ -167,7 +170,7 @@ export async function supaAuthenticateUser( return { success: false, error: "Unauthorized: Token missing", - status: 401, + status: 401 }; } @@ -196,7 +199,7 @@ export async function supaAuthenticateUser( return { success: false, error: "Unauthorized: Invalid token", - status: 401, + status: 401 }; } @@ -206,7 +209,7 @@ export async function supaAuthenticateUser( return { success: false, error: "Unauthorized: Invalid token", - status: 401, + status: 401 }; } @@ -216,7 +219,7 @@ export async function supaAuthenticateUser( const plan = getPlanByPriceId(priceId); subscriptionData = { team_id: teamId, - plan, + plan }; switch (mode) { case RateLimiterMode.Crawl: @@ -270,7 +273,13 @@ export async function supaAuthenticateUser( try { await rateLimiter.consume(team_endpoint_token); } catch (rateLimiterRes) { - logger.error(`Rate limit exceeded: ${rateLimiterRes}`, { teamId, priceId, plan: subscriptionData?.plan, mode, rateLimiterRes }); + logger.error(`Rate limit exceeded: ${rateLimiterRes}`, { + teamId, + priceId, + plan: subscriptionData?.plan, + mode, + rateLimiterRes + }); const secs = Math.round(rateLimiterRes.msBeforeNext / 1000) || 1; const retryDate = new Date(Date.now() + rateLimiterRes.msBeforeNext); @@ -284,7 +293,7 @@ export async function supaAuthenticateUser( return { success: false, error: `Rate limit exceeded. Consumed (req/min): ${rateLimiterRes.consumedPoints}, Remaining (req/min): ${rateLimiterRes.remainingPoints}. Upgrade your plan at https://firecrawl.dev/pricing for increased rate limits or please retry after ${secs}s, resets at ${retryDate}`, - status: 429, + status: 429 }; } @@ -314,7 +323,7 @@ export async function supaAuthenticateUser( success: true, team_id: teamId ?? undefined, plan: (subscriptionData?.plan ?? "") as PlanType, - chunk, + chunk }; } function getPlanByPriceId(price_id: string | null): PlanType { diff --git a/apps/api/src/controllers/v0/admin/queue.ts b/apps/api/src/controllers/v0/admin/queue.ts index 6ef8a992..6cc1c6e0 100644 --- a/apps/api/src/controllers/v0/admin/queue.ts +++ b/apps/api/src/controllers/v0/admin/queue.ts @@ -31,7 +31,9 @@ export async function cleanBefore24hCompleteJobsController( ).flat(); const before24hJobs = completedJobs.filter( - (job) => job.finishedOn !== undefined && job.finishedOn < Date.now() - 24 * 60 * 60 * 1000 + (job) => + job.finishedOn !== undefined && + job.finishedOn < Date.now() - 24 * 60 * 60 * 1000 ) || []; let count = 0; @@ -71,14 +73,14 @@ export async function queuesController(req: Request, res: Response) { const scrapeQueue = getScrapeQueue(); const [webScraperActive] = await Promise.all([ - scrapeQueue.getActiveCount(), + scrapeQueue.getActiveCount() ]); const noActiveJobs = webScraperActive === 0; // 200 if no active jobs, 503 if there are active jobs return res.status(noActiveJobs ? 200 : 500).json({ webScraperActive, - noActiveJobs, + noActiveJobs }); } catch (error) { logger.error(error); @@ -97,7 +99,7 @@ export async function autoscalerController(req: Request, res: Response) { await Promise.all([ scrapeQueue.getActiveCount(), scrapeQueue.getWaitingCount(), - scrapeQueue.getPrioritizedCount(), + scrapeQueue.getPrioritizedCount() ]); let waitingAndPriorityCount = webScraperWaiting + webScraperPriority; @@ -107,8 +109,8 @@ export async function autoscalerController(req: Request, res: Response) { "https://api.machines.dev/v1/apps/firecrawl-scraper-js/machines", { headers: { - Authorization: `Bearer ${process.env.FLY_API_TOKEN}`, - }, + Authorization: `Bearer ${process.env.FLY_API_TOKEN}` + } } ); const machines = await request.json(); @@ -184,13 +186,13 @@ export async function autoscalerController(req: Request, res: Response) { } return res.status(200).json({ mode: "scale-descale", - count: targetMachineCount, + count: targetMachineCount }); } return res.status(200).json({ mode: "normal", - count: activeMachines, + count: activeMachines }); } catch (error) { logger.error(error); diff --git a/apps/api/src/controllers/v0/admin/redis-health.ts b/apps/api/src/controllers/v0/admin/redis-health.ts index dc587606..963755ef 100644 --- a/apps/api/src/controllers/v0/admin/redis-health.ts +++ b/apps/api/src/controllers/v0/admin/redis-health.ts @@ -49,7 +49,7 @@ export async function redisHealthController(req: Request, res: Response) { const healthStatus = { queueRedis: queueRedisHealth === testValue ? "healthy" : "unhealthy", redisRateLimitClient: - redisRateLimitHealth === testValue ? "healthy" : "unhealthy", + redisRateLimitHealth === testValue ? "healthy" : "unhealthy" }; if ( diff --git a/apps/api/src/controllers/v0/crawl-cancel.ts b/apps/api/src/controllers/v0/crawl-cancel.ts index e81064f2..b445978c 100644 --- a/apps/api/src/controllers/v0/crawl-cancel.ts +++ b/apps/api/src/controllers/v0/crawl-cancel.ts @@ -10,13 +10,9 @@ configDotenv(); export async function crawlCancelController(req: Request, res: Response) { try { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; - const auth = await authenticateUser( - req, - res, - RateLimiterMode.CrawlStatus - ); + const auth = await authenticateUser(req, res, RateLimiterMode.CrawlStatus); if (!auth.success) { return res.status(auth.status).json({ error: auth.error }); } diff --git a/apps/api/src/controllers/v0/crawl-status.ts b/apps/api/src/controllers/v0/crawl-status.ts index 9c799eeb..756fca44 100644 --- a/apps/api/src/controllers/v0/crawl-status.ts +++ b/apps/api/src/controllers/v0/crawl-status.ts @@ -12,21 +12,25 @@ import { toLegacyDocument } from "../v1/types"; configDotenv(); export async function getJobs(crawlId: string, ids: string[]) { - const jobs = (await Promise.all(ids.map(x => getScrapeQueue().getJob(x)))).filter(x => x) as Job[]; - + const jobs = ( + await Promise.all(ids.map((x) => getScrapeQueue().getJob(x))) + ).filter((x) => x) as Job[]; + if (process.env.USE_DB_AUTHENTICATION === "true") { const supabaseData = await supabaseGetJobsByCrawlId(crawlId); - supabaseData.forEach(x => { - const job = jobs.find(y => y.id === x.job_id); + supabaseData.forEach((x) => { + const job = jobs.find((y) => y.id === x.job_id); if (job) { job.returnvalue = x.docs; } - }) + }); } - jobs.forEach(job => { - job.returnvalue = Array.isArray(job.returnvalue) ? job.returnvalue[0] : job.returnvalue; + jobs.forEach((job) => { + job.returnvalue = Array.isArray(job.returnvalue) + ? job.returnvalue[0] + : job.returnvalue; }); return jobs; @@ -34,11 +38,7 @@ export async function getJobs(crawlId: string, ids: string[]) { export async function crawlStatusController(req: Request, res: Response) { try { - const auth = await authenticateUser( - req, - res, - RateLimiterMode.CrawlStatus - ); + const auth = await authenticateUser(req, res, RateLimiterMode.CrawlStatus); if (!auth.success) { return res.status(auth.status).json({ error: auth.error }); } @@ -55,7 +55,7 @@ export async function crawlStatusController(req: Request, res: Response) { } let jobIDs = await getCrawlJobs(req.params.jobId); let jobs = await getJobs(req.params.jobId, jobIDs); - let jobStatuses = await Promise.all(jobs.map(x => x.getState())); + let jobStatuses = await Promise.all(jobs.map((x) => x.getState())); // Combine jobs and jobStatuses into a single array of objects let jobsWithStatuses = jobs.map((job, index) => ({ @@ -64,18 +64,31 @@ export async function crawlStatusController(req: Request, res: Response) { })); // Filter out failed jobs - jobsWithStatuses = jobsWithStatuses.filter(x => x.status !== "failed" && x.status !== "unknown"); + jobsWithStatuses = jobsWithStatuses.filter( + (x) => x.status !== "failed" && x.status !== "unknown" + ); // Sort jobs by timestamp jobsWithStatuses.sort((a, b) => a.job.timestamp - b.job.timestamp); // Extract sorted jobs and statuses - jobs = jobsWithStatuses.map(x => x.job); - jobStatuses = jobsWithStatuses.map(x => x.status); + jobs = jobsWithStatuses.map((x) => x.job); + jobStatuses = jobsWithStatuses.map((x) => x.status); - const jobStatus = sc.cancelled ? "failed" : jobStatuses.every(x => x === "completed") ? "completed" : "active"; + const jobStatus = sc.cancelled + ? "failed" + : jobStatuses.every((x) => x === "completed") + ? "completed" + : "active"; - const data = jobs.filter(x => x.failedReason !== "Concurreny limit hit" && x.returnvalue !== null).map(x => Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue); + const data = jobs + .filter( + (x) => + x.failedReason !== "Concurreny limit hit" && x.returnvalue !== null + ) + .map((x) => + Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue + ); if ( jobs.length > 0 && @@ -83,7 +96,7 @@ export async function crawlStatusController(req: Request, res: Response) { jobs[0].data.pageOptions && !jobs[0].data.pageOptions.includeRawHtml ) { - data.forEach(item => { + data.forEach((item) => { if (item) { delete item.rawHtml; } @@ -92,10 +105,19 @@ export async function crawlStatusController(req: Request, res: Response) { res.json({ status: jobStatus, - current: jobStatuses.filter(x => x === "completed" || x === "failed").length, + current: jobStatuses.filter((x) => x === "completed" || x === "failed") + .length, total: jobs.length, - data: jobStatus === "completed" ? data.map(x => toLegacyDocument(x, sc.internalOptions)) : null, - partial_data: jobStatus === "completed" ? [] : data.filter(x => x !== null).map(x => toLegacyDocument(x, sc.internalOptions)), + data: + jobStatus === "completed" + ? data.map((x) => toLegacyDocument(x, sc.internalOptions)) + : null, + partial_data: + jobStatus === "completed" + ? [] + : data + .filter((x) => x !== null) + .map((x) => toLegacyDocument(x, sc.internalOptions)) }); } catch (error) { Sentry.captureException(error); diff --git a/apps/api/src/controllers/v0/crawl.ts b/apps/api/src/controllers/v0/crawl.ts index 06a86f92..b8c6bc63 100644 --- a/apps/api/src/controllers/v0/crawl.ts +++ b/apps/api/src/controllers/v0/crawl.ts @@ -7,10 +7,22 @@ import { isUrlBlocked } from "../../../src/scraper/WebScraper/utils/blocklist"; import { logCrawl } from "../../../src/services/logging/crawl_log"; import { validateIdempotencyKey } from "../../../src/services/idempotency/validate"; import { createIdempotencyKey } from "../../../src/services/idempotency/create"; -import { defaultCrawlPageOptions, defaultCrawlerOptions, defaultOrigin } from "../../../src/lib/default-values"; +import { + defaultCrawlPageOptions, + defaultCrawlerOptions, + defaultOrigin +} from "../../../src/lib/default-values"; import { v4 as uuidv4 } from "uuid"; import { logger } from "../../../src/lib/logger"; -import { addCrawlJob, addCrawlJobs, crawlToCrawler, lockURL, lockURLs, saveCrawl, StoredCrawl } from "../../../src/lib/crawl-redis"; +import { + addCrawlJob, + addCrawlJobs, + crawlToCrawler, + lockURL, + lockURLs, + saveCrawl, + StoredCrawl +} from "../../../src/lib/crawl-redis"; import { getScrapeQueue } from "../../../src/services/queue-service"; import { checkAndUpdateURL } from "../../../src/lib/validateUrl"; import * as Sentry from "@sentry/node"; @@ -20,11 +32,7 @@ import { ZodError } from "zod"; export async function crawlController(req: Request, res: Response) { try { - const auth = await authenticateUser( - req, - res, - RateLimiterMode.Crawl - ); + const auth = await authenticateUser(req, res, RateLimiterMode.Crawl); if (!auth.success) { return res.status(auth.status).json({ error: auth.error }); } @@ -46,7 +54,7 @@ export async function crawlController(req: Request, res: Response) { const crawlerOptions = { ...defaultCrawlerOptions, - ...req.body.crawlerOptions, + ...req.body.crawlerOptions }; const pageOptions = { ...defaultCrawlPageOptions, ...req.body.pageOptions }; @@ -71,16 +79,24 @@ export async function crawlController(req: Request, res: Response) { } const limitCheck = req.body?.crawlerOptions?.limit ?? 1; - const { success: creditsCheckSuccess, message: creditsCheckMessage, remainingCredits } = - await checkTeamCredits(chunk, team_id, limitCheck); + const { + success: creditsCheckSuccess, + message: creditsCheckMessage, + remainingCredits + } = await checkTeamCredits(chunk, team_id, limitCheck); if (!creditsCheckSuccess) { - return res.status(402).json({ error: "Insufficient credits. You may be requesting with a higher limit than the amount of credits you have left. If not, upgrade your plan at https://firecrawl.dev/pricing or contact us at help@firecrawl.com" }); + return res + .status(402) + .json({ + error: + "Insufficient credits. You may be requesting with a higher limit than the amount of credits you have left. If not, upgrade your plan at https://firecrawl.dev/pricing or contact us at help@firecrawl.com" + }); } // TODO: need to do this to v1 crawlerOptions.limit = Math.min(remainingCredits, crawlerOptions.limit); - + let url = urlSchema.parse(req.body.url); if (!url) { return res.status(400).json({ error: "Url is required" }); @@ -99,7 +115,7 @@ export async function crawlController(req: Request, res: Response) { if (isUrlBlocked(url)) { return res.status(403).json({ error: - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." }); } @@ -136,7 +152,11 @@ export async function crawlController(req: Request, res: Response) { await logCrawl(id, team_id); - const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions(pageOptions, undefined, undefined); + const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions( + pageOptions, + undefined, + undefined + ); internalOptions.disableSmartWaitCache = true; // NOTE: smart wait disabled for crawls to ensure contentful scrape, speed does not matter delete (scrapeOptions as any).timeout; @@ -148,7 +168,7 @@ export async function crawlController(req: Request, res: Response) { internalOptions, team_id, plan, - createdAt: Date.now(), + createdAt: Date.now() }; const crawler = crawlToCrawler(id, sc); @@ -163,14 +183,13 @@ export async function crawlController(req: Request, res: Response) { ? null : await crawler.tryGetSitemap(); - if (sitemap !== null && sitemap.length > 0) { let jobPriority = 20; // If it is over 1000, we need to get the job priority, // otherwise we can use the default priority of 20 - if(sitemap.length > 1000){ + if (sitemap.length > 1000) { // set base to 21 - jobPriority = await getJobPriority({plan, team_id, basePriority: 21}) + jobPriority = await getJobPriority({ plan, team_id, basePriority: 21 }); } const jobs = sitemap.map((x) => { const url = x.url; @@ -187,12 +206,12 @@ export async function crawlController(req: Request, res: Response) { plan, origin: req.body.origin ?? defaultOrigin, crawl_id: id, - sitemapped: true, + sitemapped: true }, opts: { jobId: uuid, - priority: jobPriority, - }, + priority: jobPriority + } }; }); @@ -226,12 +245,12 @@ export async function crawlController(req: Request, res: Response) { team_id, plan: plan!, origin: req.body.origin ?? defaultOrigin, - crawl_id: id, + crawl_id: id }, { - priority: 15, // prioritize request 0 of crawl jobs same as scrape jobs + priority: 15 // prioritize request 0 of crawl jobs same as scrape jobs }, - jobId, + jobId ); await addCrawlJob(id, jobId); } @@ -240,8 +259,10 @@ export async function crawlController(req: Request, res: Response) { } catch (error) { Sentry.captureException(error); logger.error(error); - return res.status(500).json({ error: error instanceof ZodError - ? "Invalid URL" - : error.message }); + return res + .status(500) + .json({ + error: error instanceof ZodError ? "Invalid URL" : error.message + }); } } diff --git a/apps/api/src/controllers/v0/crawlPreview.ts b/apps/api/src/controllers/v0/crawlPreview.ts index 8b82bef8..3b47bfaa 100644 --- a/apps/api/src/controllers/v0/crawlPreview.ts +++ b/apps/api/src/controllers/v0/crawlPreview.ts @@ -4,7 +4,13 @@ import { RateLimiterMode } from "../../../src/types"; import { isUrlBlocked } from "../../../src/scraper/WebScraper/utils/blocklist"; import { v4 as uuidv4 } from "uuid"; import { logger } from "../../../src/lib/logger"; -import { addCrawlJob, crawlToCrawler, lockURL, saveCrawl, StoredCrawl } from "../../../src/lib/crawl-redis"; +import { + addCrawlJob, + crawlToCrawler, + lockURL, + saveCrawl, + StoredCrawl +} from "../../../src/lib/crawl-redis"; import { addScrapeJob } from "../../../src/services/queue-jobs"; import { checkAndUpdateURL } from "../../../src/lib/validateUrl"; import * as Sentry from "@sentry/node"; @@ -12,11 +18,7 @@ import { fromLegacyScrapeOptions } from "../v1/types"; export async function crawlPreviewController(req: Request, res: Response) { try { - const auth = await authenticateUser( - req, - res, - RateLimiterMode.Preview - ); + const auth = await authenticateUser(req, res, RateLimiterMode.Preview); const team_id = "preview"; @@ -39,16 +41,18 @@ export async function crawlPreviewController(req: Request, res: Response) { } if (isUrlBlocked(url)) { - return res - .status(403) - .json({ - error: - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", - }); + return res.status(403).json({ + error: + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + }); } const crawlerOptions = req.body.crawlerOptions ?? {}; - const pageOptions = req.body.pageOptions ?? { onlyMainContent: false, includeHtml: false, removeTags: [] }; + const pageOptions = req.body.pageOptions ?? { + onlyMainContent: false, + includeHtml: false, + removeTags: [] + }; // if (mode === "single_urls" && !url.includes(",")) { // NOTE: do we need this? // try { @@ -87,7 +91,11 @@ export async function crawlPreviewController(req: Request, res: Response) { robots = await this.getRobotsTxt(); } catch (_) {} - const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions(pageOptions, undefined, undefined); + const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions( + pageOptions, + undefined, + undefined + ); const sc: StoredCrawl = { originUrl: url, @@ -97,20 +105,44 @@ export async function crawlPreviewController(req: Request, res: Response) { team_id, plan, robots, - createdAt: Date.now(), + createdAt: Date.now() }; await saveCrawl(id, sc); const crawler = crawlToCrawler(id, sc); - const sitemap = sc.crawlerOptions?.ignoreSitemap ? null : await crawler.tryGetSitemap(); + const sitemap = sc.crawlerOptions?.ignoreSitemap + ? null + : await crawler.tryGetSitemap(); if (sitemap !== null) { - for (const url of sitemap.map(x => x.url)) { + for (const url of sitemap.map((x) => x.url)) { await lockURL(id, sc, url); const jobId = uuidv4(); - await addScrapeJob({ + await addScrapeJob( + { + url, + mode: "single_urls", + team_id, + plan: plan!, + crawlerOptions, + scrapeOptions, + internalOptions, + origin: "website-preview", + crawl_id: id, + sitemapped: true + }, + {}, + jobId + ); + await addCrawlJob(id, jobId); + } + } else { + await lockURL(id, sc, url); + const jobId = uuidv4(); + await addScrapeJob( + { url, mode: "single_urls", team_id, @@ -119,25 +151,11 @@ export async function crawlPreviewController(req: Request, res: Response) { scrapeOptions, internalOptions, origin: "website-preview", - crawl_id: id, - sitemapped: true, - }, {}, jobId); - await addCrawlJob(id, jobId); - } - } else { - await lockURL(id, sc, url); - const jobId = uuidv4(); - await addScrapeJob({ - url, - mode: "single_urls", - team_id, - plan: plan!, - crawlerOptions, - scrapeOptions, - internalOptions, - origin: "website-preview", - crawl_id: id, - }, {}, jobId); + crawl_id: id + }, + {}, + jobId + ); await addCrawlJob(id, jobId); } diff --git a/apps/api/src/controllers/v0/keyAuth.ts b/apps/api/src/controllers/v0/keyAuth.ts index 63915302..2495705c 100644 --- a/apps/api/src/controllers/v0/keyAuth.ts +++ b/apps/api/src/controllers/v0/keyAuth.ts @@ -1,17 +1,12 @@ - import { AuthResponse, RateLimiterMode } from "../../types"; import { Request, Response } from "express"; import { authenticateUser } from "../auth"; - export const keyAuthController = async (req: Request, res: Response) => { try { // make sure to authenticate user first, Bearer - const auth = await authenticateUser( - req, - res - ); + const auth = await authenticateUser(req, res); if (!auth.success) { return res.status(auth.status).json({ error: auth.error }); } @@ -22,4 +17,3 @@ export const keyAuthController = async (req: Request, res: Response) => { return res.status(500).json({ error: error.message }); } }; - diff --git a/apps/api/src/controllers/v0/scrape.ts b/apps/api/src/controllers/v0/scrape.ts index 02b9400e..c7c8d9fe 100644 --- a/apps/api/src/controllers/v0/scrape.ts +++ b/apps/api/src/controllers/v0/scrape.ts @@ -2,19 +2,24 @@ import { ExtractorOptions, PageOptions } from "./../../lib/entities"; import { Request, Response } from "express"; import { billTeam, - checkTeamCredits, + checkTeamCredits } from "../../services/billing/credit_billing"; import { authenticateUser } from "../auth"; import { PlanType, RateLimiterMode } from "../../types"; import { logJob } from "../../services/logging/log_job"; -import { Document, fromLegacyCombo, toLegacyDocument, url as urlSchema } from "../v1/types"; +import { + Document, + fromLegacyCombo, + toLegacyDocument, + url as urlSchema +} from "../v1/types"; import { isUrlBlocked } from "../../scraper/WebScraper/utils/blocklist"; // Import the isUrlBlocked function import { numTokensFromString } from "../../lib/LLM-extraction/helpers"; import { defaultPageOptions, defaultExtractorOptions, defaultTimeout, - defaultOrigin, + defaultOrigin } from "../../lib/default-values"; import { addScrapeJob, waitForJob } from "../../services/queue-jobs"; import { getScrapeQueue } from "../../services/queue-service"; @@ -50,13 +55,18 @@ export async function scrapeHelper( success: false, error: "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", - returnCode: 403, + returnCode: 403 }; } const jobPriority = await getJobPriority({ plan, team_id, basePriority: 10 }); - const { scrapeOptions, internalOptions } = fromLegacyCombo(pageOptions, extractorOptions, timeout, crawlerOptions); + const { scrapeOptions, internalOptions } = fromLegacyCombo( + pageOptions, + extractorOptions, + timeout, + crawlerOptions + ); await addScrapeJob( { @@ -67,7 +77,7 @@ export async function scrapeHelper( internalOptions, plan: plan!, origin: req.body.origin ?? defaultOrigin, - is_scrape: true, + is_scrape: true }, {}, jobId, @@ -80,18 +90,21 @@ export async function scrapeHelper( { name: "Wait for job to finish", op: "bullmq.wait", - attributes: { job: jobId }, + attributes: { job: jobId } }, async (span) => { try { - doc = (await waitForJob(jobId, timeout)); + doc = await waitForJob(jobId, timeout); } catch (e) { - if (e instanceof Error && (e.message.startsWith("Job wait") || e.message === "timeout")) { + if ( + e instanceof Error && + (e.message.startsWith("Job wait") || e.message === "timeout") + ) { span.setAttribute("timedOut", true); return { success: false, error: "Request timed out", - returnCode: 408, + returnCode: 408 }; } else if ( typeof e === "string" && @@ -104,7 +117,7 @@ export async function scrapeHelper( return { success: false, error: e, - returnCode: 500, + returnCode: 500 }; } else { throw e; @@ -127,7 +140,7 @@ export async function scrapeHelper( success: true, error: "No page found", returnCode: 200, - data: doc, + data: doc }; } @@ -153,7 +166,7 @@ export async function scrapeHelper( return { success: true, data: toLegacyDocument(doc, internalOptions), - returnCode: 200, + returnCode: 200 }; } @@ -161,11 +174,7 @@ export async function scrapeController(req: Request, res: Response) { try { let earlyReturn = false; // make sure to authenticate user first, Bearer - const auth = await authenticateUser( - req, - res, - RateLimiterMode.Scrape - ); + const auth = await authenticateUser(req, res, RateLimiterMode.Scrape); if (!auth.success) { return res.status(auth.status).json({ error: auth.error }); } @@ -176,7 +185,7 @@ export async function scrapeController(req: Request, res: Response) { const pageOptions = { ...defaultPageOptions, ...req.body.pageOptions }; const extractorOptions = { ...defaultExtractorOptions, - ...req.body.extractorOptions, + ...req.body.extractorOptions }; const origin = req.body.origin ?? defaultOrigin; let timeout = req.body.timeout ?? defaultTimeout; @@ -188,7 +197,7 @@ export async function scrapeController(req: Request, res: Response) { ) { return res.status(400).json({ error: - "extractorOptions.extractionSchema must be an object if llm-extraction mode is specified", + "extractorOptions.extractionSchema must be an object if llm-extraction mode is specified" }); } @@ -202,14 +211,19 @@ export async function scrapeController(req: Request, res: Response) { await checkTeamCredits(chunk, team_id, 1); if (!creditsCheckSuccess) { earlyReturn = true; - return res.status(402).json({ error: "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing" }); + return res + .status(402) + .json({ + error: + "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing" + }); } } catch (error) { logger.error(error); earlyReturn = true; return res.status(500).json({ error: - "Error checking team credits. Please contact help@firecrawl.com for help.", + "Error checking team credits. Please contact help@firecrawl.com for help." }); } @@ -230,7 +244,10 @@ export async function scrapeController(req: Request, res: Response) { const timeTakenInSeconds = (endTime - startTime) / 1000; const numTokens = result.data && (result.data as Document).markdown - ? numTokensFromString((result.data as Document).markdown!, "gpt-3.5-turbo") + ? numTokensFromString( + (result.data as Document).markdown!, + "gpt-3.5-turbo" + ) : 0; if (result.success) { @@ -250,27 +267,33 @@ export async function scrapeController(req: Request, res: Response) { } if (creditsToBeBilled > 0) { // billing for doc done on queue end, bill only for llm extraction - billTeam(team_id, chunk?.sub_id, creditsToBeBilled).catch(error => { - logger.error(`Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`); + billTeam(team_id, chunk?.sub_id, creditsToBeBilled).catch((error) => { + logger.error( + `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}` + ); // Optionally, you could notify an admin or add to a retry queue here }); } } - + let doc = result.data; if (!pageOptions || !pageOptions.includeRawHtml) { if (doc && (doc as Document).rawHtml) { delete (doc as Document).rawHtml; } } - - if(pageOptions && pageOptions.includeExtract) { - if(!pageOptions.includeMarkdown && doc && (doc as Document).markdown) { + + if (pageOptions && pageOptions.includeExtract) { + if (!pageOptions.includeMarkdown && doc && (doc as Document).markdown) { delete (doc as Document).markdown; } } - const { scrapeOptions } = fromLegacyScrapeOptions(pageOptions, extractorOptions, timeout); + const { scrapeOptions } = fromLegacyScrapeOptions( + pageOptions, + extractorOptions, + timeout + ); logJob({ job_id: jobId, @@ -285,7 +308,7 @@ export async function scrapeController(req: Request, res: Response) { crawlerOptions: crawlerOptions, scrapeOptions, origin: origin, - num_tokens: numTokens, + num_tokens: numTokens }); return res.status(result.returnCode).json(result); @@ -298,7 +321,7 @@ export async function scrapeController(req: Request, res: Response) { ? "Invalid URL" : typeof error === "string" ? error - : error?.message ?? "Internal Server Error", + : (error?.message ?? "Internal Server Error") }); } } diff --git a/apps/api/src/controllers/v0/search.ts b/apps/api/src/controllers/v0/search.ts index 4dd38afd..4950ea5f 100644 --- a/apps/api/src/controllers/v0/search.ts +++ b/apps/api/src/controllers/v0/search.ts @@ -1,5 +1,8 @@ import { Request, Response } from "express"; -import { billTeam, checkTeamCredits } from "../../services/billing/credit_billing"; +import { + billTeam, + checkTeamCredits +} from "../../services/billing/credit_billing"; import { authenticateUser } from "../auth"; import { PlanType, RateLimiterMode } from "../../types"; import { logJob } from "../../services/logging/log_job"; @@ -13,7 +16,12 @@ import { addScrapeJob, waitForJob } from "../../services/queue-jobs"; import * as Sentry from "@sentry/node"; import { getJobPriority } from "../../lib/job-priority"; import { Job } from "bullmq"; -import { Document, fromLegacyCombo, fromLegacyScrapeOptions, toLegacyDocument } from "../v1/types"; +import { + Document, + fromLegacyCombo, + fromLegacyScrapeOptions, + toLegacyDocument +} from "../v1/types"; export async function searchHelper( jobId: string, @@ -54,16 +62,23 @@ export async function searchHelper( filter: filter, lang: searchOptions.lang ?? "en", country: searchOptions.country ?? "us", - location: searchOptions.location, + location: searchOptions.location }); let justSearch = pageOptions.fetchPageContent === false; - const { scrapeOptions, internalOptions } = fromLegacyCombo(pageOptions, undefined, 60000, crawlerOptions); + const { scrapeOptions, internalOptions } = fromLegacyCombo( + pageOptions, + undefined, + 60000, + crawlerOptions + ); if (justSearch) { - billTeam(team_id, subscription_id, res.length).catch(error => { - logger.error(`Failed to bill team ${team_id} for ${res.length} credits: ${error}`); + billTeam(team_id, subscription_id, res.length).catch((error) => { + logger.error( + `Failed to bill team ${team_id} for ${res.length} credits: ${error}` + ); // Optionally, you could notify an admin or add to a retry queue here }); return { success: true, data: res, returnCode: 200 }; @@ -78,11 +93,11 @@ export async function searchHelper( return { success: true, error: "No search results found", returnCode: 200 }; } - const jobPriority = await getJobPriority({plan, team_id, basePriority: 20}); - + const jobPriority = await getJobPriority({ plan, team_id, basePriority: 20 }); + // filter out social media links - const jobDatas = res.map(x => { + const jobDatas = res.map((x) => { const url = x.url; const uuid = uuidv4(); return { @@ -92,28 +107,32 @@ export async function searchHelper( mode: "single_urls", team_id: team_id, scrapeOptions, - internalOptions, + internalOptions }, opts: { jobId: uuid, - priority: jobPriority, + priority: jobPriority } }; - }) + }); // TODO: addScrapeJobs for (const job of jobDatas) { - await addScrapeJob(job.data as any, {}, job.opts.jobId, job.opts.priority) + await addScrapeJob(job.data as any, {}, job.opts.jobId, job.opts.priority); } - const docs = (await Promise.all(jobDatas.map(x => waitForJob(x.opts.jobId, 60000)))).map(x => toLegacyDocument(x, internalOptions)); - + const docs = ( + await Promise.all( + jobDatas.map((x) => waitForJob(x.opts.jobId, 60000)) + ) + ).map((x) => toLegacyDocument(x, internalOptions)); + if (docs.length === 0) { return { success: true, error: "No search results found", returnCode: 200 }; } const sq = getScrapeQueue(); - await Promise.all(jobDatas.map(x => sq.remove(x.opts.jobId))); + await Promise.all(jobDatas.map((x) => sq.remove(x.opts.jobId))); // make sure doc.content is not empty const filteredDocs = docs.filter( @@ -121,24 +140,25 @@ export async function searchHelper( ); if (filteredDocs.length === 0) { - return { success: true, error: "No page found", returnCode: 200, data: docs }; + return { + success: true, + error: "No page found", + returnCode: 200, + data: docs + }; } return { success: true, data: filteredDocs, - returnCode: 200, + returnCode: 200 }; } export async function searchController(req: Request, res: Response) { try { // make sure to authenticate user first, Bearer - const auth = await authenticateUser( - req, - res, - RateLimiterMode.Search - ); + const auth = await authenticateUser(req, res, RateLimiterMode.Search); if (!auth.success) { return res.status(auth.status).json({ error: auth.error }); } @@ -149,12 +169,12 @@ export async function searchController(req: Request, res: Response) { onlyMainContent: req.body.pageOptions?.onlyMainContent ?? false, fetchPageContent: req.body.pageOptions?.fetchPageContent ?? true, removeTags: req.body.pageOptions?.removeTags ?? [], - fallback: req.body.pageOptions?.fallback ?? false, + fallback: req.body.pageOptions?.fallback ?? false }; const origin = req.body.origin ?? "api"; const searchOptions = req.body.searchOptions ?? { limit: 5 }; - + const jobId = uuidv4(); try { @@ -192,11 +212,14 @@ export async function searchController(req: Request, res: Response) { mode: "search", url: req.body.query, crawlerOptions: crawlerOptions, - origin: origin, + origin: origin }); return res.status(result.returnCode).json(result); } catch (error) { - if (error instanceof Error && (error.message.startsWith("Job wait") || error.message === "timeout")) { + if ( + error instanceof Error && + (error.message.startsWith("Job wait") || error.message === "timeout") + ) { return res.status(408).json({ error: "Request timed out" }); } diff --git a/apps/api/src/controllers/v0/status.ts b/apps/api/src/controllers/v0/status.ts index c5eafc2d..73bfa159 100644 --- a/apps/api/src/controllers/v0/status.ts +++ b/apps/api/src/controllers/v0/status.ts @@ -4,7 +4,10 @@ import { getCrawl, getCrawlJobs } from "../../../src/lib/crawl-redis"; import { getJobs } from "./crawl-status"; import * as Sentry from "@sentry/node"; -export async function crawlJobStatusPreviewController(req: Request, res: Response) { +export async function crawlJobStatusPreviewController( + req: Request, + res: Response +) { try { const sc = await getCrawl(req.params.jobId); if (!sc) { @@ -22,18 +25,30 @@ export async function crawlJobStatusPreviewController(req: Request, res: Respons // } // } - const jobs = (await getJobs(req.params.jobId, jobIDs)).sort((a, b) => a.timestamp - b.timestamp); - const jobStatuses = await Promise.all(jobs.map(x => x.getState())); - const jobStatus = sc.cancelled ? "failed" : jobStatuses.every(x => x === "completed") ? "completed" : jobStatuses.some(x => x === "failed") ? "failed" : "active"; + const jobs = (await getJobs(req.params.jobId, jobIDs)).sort( + (a, b) => a.timestamp - b.timestamp + ); + const jobStatuses = await Promise.all(jobs.map((x) => x.getState())); + const jobStatus = sc.cancelled + ? "failed" + : jobStatuses.every((x) => x === "completed") + ? "completed" + : jobStatuses.some((x) => x === "failed") + ? "failed" + : "active"; - const data = jobs.map(x => Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue); + const data = jobs.map((x) => + Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue + ); res.json({ status: jobStatus, - current: jobStatuses.filter(x => x === "completed" || x === "failed").length, + current: jobStatuses.filter((x) => x === "completed" || x === "failed") + .length, total: jobs.length, data: jobStatus === "completed" ? data : null, - partial_data: jobStatus === "completed" ? [] : data.filter(x => x !== null), + partial_data: + jobStatus === "completed" ? [] : data.filter((x) => x !== null) }); } catch (error) { Sentry.captureException(error); diff --git a/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts b/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts index 0a9931d3..1ce058a0 100644 --- a/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts +++ b/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts @@ -24,11 +24,15 @@ describe("URL Schema Validation", () => { }); it("should reject URLs without a valid top-level domain", () => { - expect(() => url.parse("http://example")).toThrow("URL must have a valid top-level domain or be a valid path"); + expect(() => url.parse("http://example")).toThrow( + "URL must have a valid top-level domain or be a valid path" + ); }); it("should reject blocked URLs", () => { - expect(() => url.parse("https://facebook.com")).toThrow("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); + expect(() => url.parse("https://facebook.com")).toThrow( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); }); it("should handle URLs with subdomains correctly", () => { @@ -42,23 +46,33 @@ describe("URL Schema Validation", () => { }); it("should handle URLs with subdomains that are blocked", () => { - expect(() => url.parse("https://sub.facebook.com")).toThrow("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); + expect(() => url.parse("https://sub.facebook.com")).toThrow( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); }); it("should handle URLs with paths that are blocked", () => { - expect(() => url.parse("http://facebook.com/path")).toThrow("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); - expect(() => url.parse("https://facebook.com/another/path")).toThrow("Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it."); + expect(() => url.parse("http://facebook.com/path")).toThrow( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); + expect(() => url.parse("https://facebook.com/another/path")).toThrow( + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + ); }); - + it("should reject malformed URLs starting with 'http://http'", () => { - expect(() => url.parse("http://http://example.com")).toThrow("Invalid URL. Invalid protocol."); + expect(() => url.parse("http://http://example.com")).toThrow( + "Invalid URL. Invalid protocol." + ); }); it("should reject malformed URLs containing multiple 'http://'", () => { - expect(() => url.parse("http://example.com/http://example.com")).not.toThrow(); + expect(() => + url.parse("http://example.com/http://example.com") + ).not.toThrow(); }); it("should reject malformed URLs containing multiple 'http://'", () => { expect(() => url.parse("http://ex ample.com/")).toThrow("Invalid URL"); }); -}) \ No newline at end of file +}); diff --git a/apps/api/src/controllers/v1/batch-scrape.ts b/apps/api/src/controllers/v1/batch-scrape.ts index 064ee73b..a78264e3 100644 --- a/apps/api/src/controllers/v1/batch-scrape.ts +++ b/apps/api/src/controllers/v1/batch-scrape.ts @@ -5,14 +5,14 @@ import { batchScrapeRequestSchema, CrawlResponse, RequestWithAuth, - ScrapeOptions, + ScrapeOptions } from "./types"; import { addCrawlJobs, getCrawl, lockURLs, saveCrawl, - StoredCrawl, + StoredCrawl } from "../../lib/crawl-redis"; import { logCrawl } from "../../services/logging/crawl_log"; import { getJobPriority } from "../../lib/job-priority"; @@ -27,27 +27,40 @@ export async function batchScrapeController( req.body = batchScrapeRequestSchema.parse(req.body); const id = req.body.appendToId ?? uuidv4(); - const logger = _logger.child({ crawlId: id, batchScrapeId: id, module: "api/v1", method: "batchScrapeController", teamId: req.auth.team_id, plan: req.auth.plan }); - logger.debug("Batch scrape " + id + " starting", { urlsLength: req.body.urls, appendToId: req.body.appendToId, account: req.account }); + const logger = _logger.child({ + crawlId: id, + batchScrapeId: id, + module: "api/v1", + method: "batchScrapeController", + teamId: req.auth.team_id, + plan: req.auth.plan + }); + logger.debug("Batch scrape " + id + " starting", { + urlsLength: req.body.urls, + appendToId: req.body.appendToId, + account: req.account + }); if (!req.body.appendToId) { await logCrawl(id, req.auth.team_id); } let { remainingCredits } = req.account!; - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; - if(!useDbAuthentication){ + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; + if (!useDbAuthentication) { remainingCredits = Infinity; } - const sc: StoredCrawl = req.body.appendToId ? await getCrawl(req.body.appendToId) as StoredCrawl : { - crawlerOptions: null, - scrapeOptions: req.body, - internalOptions: { disableSmartWaitCache: true }, // NOTE: smart wait disabled for batch scrapes to ensure contentful scrape, speed does not matter - team_id: req.auth.team_id, - createdAt: Date.now(), - plan: req.auth.plan, - }; + const sc: StoredCrawl = req.body.appendToId + ? ((await getCrawl(req.body.appendToId)) as StoredCrawl) + : { + crawlerOptions: null, + scrapeOptions: req.body, + internalOptions: { disableSmartWaitCache: true }, // NOTE: smart wait disabled for batch scrapes to ensure contentful scrape, speed does not matter + team_id: req.auth.team_id, + createdAt: Date.now(), + plan: req.auth.plan + }; if (!req.body.appendToId) { await saveCrawl(id, sc); @@ -57,9 +70,13 @@ export async function batchScrapeController( // If it is over 1000, we need to get the job priority, // otherwise we can use the default priority of 20 - if(req.body.urls.length > 1000){ + if (req.body.urls.length > 1000) { // set base to 21 - jobPriority = await getJobPriority({plan: req.auth.plan, team_id: req.auth.team_id, basePriority: 21}) + jobPriority = await getJobPriority({ + plan: req.auth.plan, + team_id: req.auth.team_id, + basePriority: 21 + }); } logger.debug("Using job priority " + jobPriority, { jobPriority }); @@ -80,12 +97,12 @@ export async function batchScrapeController( crawl_id: id, sitemapped: true, v1: true, - webhook: req.body.webhook, + webhook: req.body.webhook }, opts: { jobId: uuidv4(), - priority: 20, - }, + priority: 20 + } }; }); @@ -103,18 +120,25 @@ export async function batchScrapeController( logger.debug("Adding scrape jobs to BullMQ..."); await addScrapeJobs(jobs); - if(req.body.webhook) { - logger.debug("Calling webhook with batch_scrape.started...", { webhook: req.body.webhook }); - await callWebhook(req.auth.team_id, id, null, req.body.webhook, true, "batch_scrape.started"); + if (req.body.webhook) { + logger.debug("Calling webhook with batch_scrape.started...", { + webhook: req.body.webhook + }); + await callWebhook( + req.auth.team_id, + id, + null, + req.body.webhook, + true, + "batch_scrape.started" + ); } const protocol = process.env.ENV === "local" ? req.protocol : "https"; - + return res.status(200).json({ success: true, id, - url: `${protocol}://${req.get("host")}/v1/batch/scrape/${id}`, + url: `${protocol}://${req.get("host")}/v1/batch/scrape/${id}` }); } - - diff --git a/apps/api/src/controllers/v1/concurrency-check.ts b/apps/api/src/controllers/v1/concurrency-check.ts index 8695c6e6..bd25c73b 100644 --- a/apps/api/src/controllers/v1/concurrency-check.ts +++ b/apps/api/src/controllers/v1/concurrency-check.ts @@ -2,7 +2,7 @@ import { authenticateUser } from "../auth"; import { ConcurrencyCheckParams, ConcurrencyCheckResponse, - RequestWithAuth, + RequestWithAuth } from "./types"; import { RateLimiterMode } from "../../types"; import { Response } from "express"; diff --git a/apps/api/src/controllers/v1/crawl-cancel.ts b/apps/api/src/controllers/v1/crawl-cancel.ts index 958318b5..986ff104 100644 --- a/apps/api/src/controllers/v1/crawl-cancel.ts +++ b/apps/api/src/controllers/v1/crawl-cancel.ts @@ -7,9 +7,12 @@ import { configDotenv } from "dotenv"; import { RequestWithAuth } from "./types"; configDotenv(); -export async function crawlCancelController(req: RequestWithAuth<{ jobId: string }>, res: Response) { +export async function crawlCancelController( + req: RequestWithAuth<{ jobId: string }>, + res: Response +) { try { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; const sc = await getCrawl(req.params.jobId); if (!sc) { diff --git a/apps/api/src/controllers/v1/crawl-status-ws.ts b/apps/api/src/controllers/v1/crawl-status-ws.ts index f552492f..d9994d97 100644 --- a/apps/api/src/controllers/v1/crawl-status-ws.ts +++ b/apps/api/src/controllers/v1/crawl-status-ws.ts @@ -1,32 +1,47 @@ import { authMiddleware } from "../../routes/v1"; import { RateLimiterMode } from "../../types"; import { authenticateUser } from "../auth"; -import { CrawlStatusParams, CrawlStatusResponse, Document, ErrorResponse, RequestWithAuth } from "./types"; +import { + CrawlStatusParams, + CrawlStatusResponse, + Document, + ErrorResponse, + RequestWithAuth +} from "./types"; import { WebSocket } from "ws"; import { v4 as uuidv4 } from "uuid"; import { logger } from "../../lib/logger"; -import { getCrawl, getCrawlExpiry, getCrawlJobs, getDoneJobsOrdered, getDoneJobsOrderedLength, getThrottledJobs, isCrawlFinished, isCrawlFinishedLocked } from "../../lib/crawl-redis"; +import { + getCrawl, + getCrawlExpiry, + getCrawlJobs, + getDoneJobsOrdered, + getDoneJobsOrderedLength, + getThrottledJobs, + isCrawlFinished, + isCrawlFinishedLocked +} from "../../lib/crawl-redis"; import { getScrapeQueue } from "../../services/queue-service"; import { getJob, getJobs } from "./crawl-status"; import * as Sentry from "@sentry/node"; import { Job, JobState } from "bullmq"; type ErrorMessage = { - type: "error", - error: string, -} + type: "error"; + error: string; +}; type CatchupMessage = { - type: "catchup", - data: CrawlStatusResponse, -} + type: "catchup"; + data: CrawlStatusResponse; +}; type DocumentMessage = { - type: "document", - data: Document, -} + type: "document"; + data: Document; +}; -type DoneMessage = { type: "done" } +type DoneMessage = { type: "done" }; type Message = ErrorMessage | CatchupMessage | DoneMessage | DocumentMessage; @@ -47,7 +62,10 @@ function close(ws: WebSocket, code: number, msg: Message) { } } -async function crawlStatusWS(ws: WebSocket, req: RequestWithAuth) { +async function crawlStatusWS( + ws: WebSocket, + req: RequestWithAuth +) { const sc = await getCrawl(req.params.jobId); if (!sc) { return close(ws, 1008, { type: "error", error: "Job not found" }); @@ -69,17 +87,23 @@ async function crawlStatusWS(ws: WebSocket, req: RequestWithAuth !doneJobIDs.includes(x)); - const jobStatuses = await Promise.all(notDoneJobIDs.map(async x => [x, await getScrapeQueue().getJobState(x)])); - const newlyDoneJobIDs: string[] = jobStatuses.filter(x => x[1] === "completed" || x[1] === "failed").map(x => x[0]); - const newlyDoneJobs: Job[] = (await Promise.all(newlyDoneJobIDs.map(x => getJob(x)))).filter(x => x !== undefined) as Job[] + const notDoneJobIDs = jobIDs.filter((x) => !doneJobIDs.includes(x)); + const jobStatuses = await Promise.all( + notDoneJobIDs.map(async (x) => [x, await getScrapeQueue().getJobState(x)]) + ); + const newlyDoneJobIDs: string[] = jobStatuses + .filter((x) => x[1] === "completed" || x[1] === "failed") + .map((x) => x[0]); + const newlyDoneJobs: Job[] = ( + await Promise.all(newlyDoneJobIDs.map((x) => getJob(x))) + ).filter((x) => x !== undefined) as Job[]; for (const job of newlyDoneJobs) { if (job.returnvalue) { send(ws, { type: "document", - data: job.returnvalue, - }) + data: job.returnvalue + }); } else { return close(ws, 3000, { type: "error", error: job.failedReason }); } @@ -95,8 +119,10 @@ async function crawlStatusWS(ws: WebSocket, req: RequestWithAuth [x, await getScrapeQueue().getJobState(x)] as const)); - const throttledJobs = new Set(...await getThrottledJobs(req.auth.team_id)); + let jobStatuses = await Promise.all( + jobIDs.map(async (x) => [x, await getScrapeQueue().getJobState(x)] as const) + ); + const throttledJobs = new Set(...(await getThrottledJobs(req.auth.team_id))); const throttledJobsSet = new Set(throttledJobs); @@ -104,18 +130,27 @@ async function crawlStatusWS(ws: WebSocket, req: RequestWithAuth["status"] = sc.cancelled ? "cancelled" : validJobStatuses.every(x => x[1] === "completed") ? "completed" : "scraping"; + const status: Exclude["status"] = + sc.cancelled + ? "cancelled" + : validJobStatuses.every((x) => x[1] === "completed") + ? "completed" + : "scraping"; jobIDs = validJobIDs; // Use validJobIDs instead of jobIDs for further processing const doneJobs = await getJobs(doneJobIDs); - const data = doneJobs.map(x => x.returnvalue); + const data = doneJobs.map((x) => x.returnvalue); send(ws, { type: "catchup", @@ -126,7 +161,7 @@ async function crawlStatusWS(ws: WebSocket, req: RequestWithAuth) { +export async function crawlStatusWSController( + ws: WebSocket, + req: RequestWithAuth +) { try { - const auth = await authenticateUser( - req, - null, - RateLimiterMode.CrawlStatus, - ); + const auth = await authenticateUser(req, null, RateLimiterMode.CrawlStatus); if (!auth.success) { return close(ws, 3000, { type: "error", - error: auth.error, + error: auth.error }); } @@ -167,15 +201,24 @@ export async function crawlStatusWSController(ws: WebSocket, req: RequestWithAut verbose = JSON.stringify({ message: err.message, name: err.name, - stack: err.stack, + stack: err.stack }); } } - logger.error("Error occurred in WebSocket! (" + req.path + ") -- ID " + id + " -- " + verbose); + logger.error( + "Error occurred in WebSocket! (" + + req.path + + ") -- ID " + + id + + " -- " + + verbose + ); return close(ws, 1011, { type: "error", - error: "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + id + error: + "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + + id }); } } diff --git a/apps/api/src/controllers/v1/crawl-status.ts b/apps/api/src/controllers/v1/crawl-status.ts index c0c4f4b5..d88d26fb 100644 --- a/apps/api/src/controllers/v1/crawl-status.ts +++ b/apps/api/src/controllers/v1/crawl-status.ts @@ -1,8 +1,23 @@ import { Response } from "express"; -import { CrawlStatusParams, CrawlStatusResponse, ErrorResponse, RequestWithAuth } from "./types"; -import { getCrawl, getCrawlExpiry, getCrawlJobs, getDoneJobsOrdered, getDoneJobsOrderedLength, getThrottledJobs } from "../../lib/crawl-redis"; +import { + CrawlStatusParams, + CrawlStatusResponse, + ErrorResponse, + RequestWithAuth +} from "./types"; +import { + getCrawl, + getCrawlExpiry, + getCrawlJobs, + getDoneJobsOrdered, + getDoneJobsOrderedLength, + getThrottledJobs +} from "../../lib/crawl-redis"; import { getScrapeQueue } from "../../services/queue-service"; -import { supabaseGetJobById, supabaseGetJobsById } from "../../lib/supabase-jobs"; +import { + supabaseGetJobById, + supabaseGetJobsById +} from "../../lib/supabase-jobs"; import { configDotenv } from "dotenv"; import { Job, JobState } from "bullmq"; import { logger } from "../../lib/logger"; @@ -11,7 +26,7 @@ configDotenv(); export async function getJob(id: string) { const job = await getScrapeQueue().getJob(id); if (!job) return job; - + if (process.env.USE_DB_AUTHENTICATION === "true") { const supabaseData = await supabaseGetJobById(id); @@ -20,33 +35,43 @@ export async function getJob(id: string) { } } - job.returnvalue = Array.isArray(job.returnvalue) ? job.returnvalue[0] : job.returnvalue; + job.returnvalue = Array.isArray(job.returnvalue) + ? job.returnvalue[0] + : job.returnvalue; return job; } export async function getJobs(ids: string[]) { - const jobs: (Job & { id: string })[] = (await Promise.all(ids.map(x => getScrapeQueue().getJob(x)))).filter(x => x) as (Job & {id: string})[]; - + const jobs: (Job & { id: string })[] = ( + await Promise.all(ids.map((x) => getScrapeQueue().getJob(x))) + ).filter((x) => x) as (Job & { id: string })[]; + if (process.env.USE_DB_AUTHENTICATION === "true") { const supabaseData = await supabaseGetJobsById(ids); - supabaseData.forEach(x => { - const job = jobs.find(y => y.id === x.job_id); + supabaseData.forEach((x) => { + const job = jobs.find((y) => y.id === x.job_id); if (job) { job.returnvalue = x.docs; } - }) + }); } - jobs.forEach(job => { - job.returnvalue = Array.isArray(job.returnvalue) ? job.returnvalue[0] : job.returnvalue; + jobs.forEach((job) => { + job.returnvalue = Array.isArray(job.returnvalue) + ? job.returnvalue[0] + : job.returnvalue; }); return jobs; } -export async function crawlStatusController(req: RequestWithAuth, res: Response, isBatch = false) { +export async function crawlStatusController( + req: RequestWithAuth, + res: Response, + isBatch = false +) { const sc = await getCrawl(req.params.jobId); if (!sc) { return res.status(404).json({ success: false, error: "Job not found" }); @@ -56,12 +81,18 @@ export async function crawlStatusController(req: RequestWithAuth [x, await getScrapeQueue().getJobState(x)] as const)); - const throttledJobs = new Set(...await getThrottledJobs(req.auth.team_id)); + let jobStatuses = await Promise.all( + jobIDs.map(async (x) => [x, await getScrapeQueue().getJobState(x)] as const) + ); + const throttledJobs = new Set(...(await getThrottledJobs(req.auth.team_id))); const throttledJobsSet = new Set(throttledJobs); @@ -69,30 +100,48 @@ export async function crawlStatusController(req: RequestWithAuth["status"] = sc.cancelled ? "cancelled" : validJobStatuses.every(x => x[1] === "completed") ? "completed" : "scraping"; + const status: Exclude["status"] = + sc.cancelled + ? "cancelled" + : validJobStatuses.every((x) => x[1] === "completed") + ? "completed" + : "scraping"; // Use validJobIDs instead of jobIDs for further processing jobIDs = validJobIDs; const doneJobsLength = await getDoneJobsOrderedLength(req.params.jobId); - const doneJobsOrder = await getDoneJobsOrdered(req.params.jobId, start, end ?? -1); + const doneJobsOrder = await getDoneJobsOrdered( + req.params.jobId, + start, + end ?? -1 + ); let doneJobs: Job[] = []; - if (end === undefined) { // determine 10 megabyte limit + if (end === undefined) { + // determine 10 megabyte limit let bytes = 0; const bytesLimit = 10485760; // 10 MiB in bytes const factor = 100; // chunking for faster retrieval - for (let i = 0; i < doneJobsOrder.length && bytes < bytesLimit; i += factor) { + for ( + let i = 0; + i < doneJobsOrder.length && bytes < bytesLimit; + i += factor + ) { // get current chunk and retrieve jobs - const currentIDs = doneJobsOrder.slice(i, i+factor); + const currentIDs = doneJobsOrder.slice(i, i + factor); const jobs = await getJobs(currentIDs); // iterate through jobs and add them one them one to the byte counter @@ -101,12 +150,16 @@ export async function crawlStatusController(req: RequestWithAuth (await x.getState()) === "failed" ? null : x))).filter(x => x !== null) as Job[]; + doneJobs = ( + await Promise.all( + (await getJobs(doneJobsOrder)).map(async (x) => + (await x.getState()) === "failed" ? null : x + ) + ) + ).filter((x) => x !== null) as Job[]; } - const data = doneJobs.map(x => x.returnvalue); + const data = doneJobs.map((x) => x.returnvalue); const protocol = process.env.ENV === "local" ? req.protocol : "https"; - const nextURL = new URL(`${protocol}://${req.get("host")}/v1/${isBatch ? "batch/scrape" : "crawl"}/${req.params.jobId}`); + const nextURL = new URL( + `${protocol}://${req.get("host")}/v1/${isBatch ? "batch/scrape" : "crawl"}/${req.params.jobId}` + ); nextURL.searchParams.set("skip", (start + data.length).toString()); @@ -151,10 +212,9 @@ export async function crawlStatusController(req: RequestWithAuth 0) { - logger.debug("Using sitemap of length " + sitemap.length, { sitemapLength: sitemap.length }); + logger.debug("Using sitemap of length " + sitemap.length, { + sitemapLength: sitemap.length + }); let jobPriority = 20; // If it is over 1000, we need to get the job priority, // otherwise we can use the default priority of 20 - if(sitemap.length > 1000){ + if (sitemap.length > 1000) { // set base to 21 - jobPriority = await getJobPriority({plan: req.auth.plan, team_id: req.auth.team_id, basePriority: 21}) + jobPriority = await getJobPriority({ + plan: req.auth.plan, + team_id: req.auth.team_id, + basePriority: 21 + }); } logger.debug("Using job priority " + jobPriority, { jobPriority }); @@ -127,14 +149,14 @@ export async function crawlController( crawl_id: id, sitemapped: true, webhook: req.body.webhook, - v1: true, + v1: true }, opts: { jobId: uuid, - priority: 20, - }, + priority: 20 + } }; - }) + }); logger.debug("Locking URLs..."); await lockURLs( @@ -150,7 +172,9 @@ export async function crawlController( logger.debug("Adding scrape jobs to BullMQ..."); await getScrapeQueue().addBulk(jobs); } else { - logger.debug("Sitemap not found or ignored.", { ignoreSitemap: sc.crawlerOptions.ignoreSitemap }); + logger.debug("Sitemap not found or ignored.", { + ignoreSitemap: sc.crawlerOptions.ignoreSitemap + }); logger.debug("Locking URL..."); await lockURL(id, sc, req.body.url); @@ -168,30 +192,37 @@ export async function crawlController( origin: "api", crawl_id: id, webhook: req.body.webhook, - v1: true, + v1: true }, { - priority: 15, + priority: 15 }, - jobId, + jobId ); logger.debug("Adding scrape job to BullMQ...", { jobId }); await addCrawlJob(id, jobId); } logger.debug("Done queueing jobs!"); - if(req.body.webhook) { - logger.debug("Calling webhook with crawl.started...", { webhook: req.body.webhook }); - await callWebhook(req.auth.team_id, id, null, req.body.webhook, true, "crawl.started"); + if (req.body.webhook) { + logger.debug("Calling webhook with crawl.started...", { + webhook: req.body.webhook + }); + await callWebhook( + req.auth.team_id, + id, + null, + req.body.webhook, + true, + "crawl.started" + ); } const protocol = process.env.ENV === "local" ? req.protocol : "https"; - + return res.status(200).json({ success: true, id, - url: `${protocol}://${req.get("host")}/v1/crawl/${id}`, + url: `${protocol}://${req.get("host")}/v1/crawl/${id}` }); } - - diff --git a/apps/api/src/controllers/v1/extract.ts b/apps/api/src/controllers/v1/extract.ts index 736c8760..74b188e7 100644 --- a/apps/api/src/controllers/v1/extract.ts +++ b/apps/api/src/controllers/v1/extract.ts @@ -6,7 +6,7 @@ import { extractRequestSchema, ExtractResponse, MapDocument, - scrapeOptions, + scrapeOptions } from "./types"; import { Document } from "../../lib/entities"; import Redis from "ioredis"; @@ -46,7 +46,7 @@ export async function extractController( res: Response ) { const selfHosted = process.env.USE_DB_AUTHENTICATION !== "true"; - + req.body = extractRequestSchema.parse(req.body); const id = crypto.randomUUID(); @@ -56,17 +56,19 @@ export async function extractController( // Process all URLs in parallel const urlPromises = req.body.urls.map(async (url) => { - if (url.includes('/*') || req.body.allowExternalLinks) { + if (url.includes("/*") || req.body.allowExternalLinks) { // Handle glob pattern URLs - const baseUrl = url.replace('/*', ''); + const baseUrl = url.replace("/*", ""); // const pathPrefix = baseUrl.split('/').slice(3).join('/'); // Get path after domain if any const allowExternalLinks = req.body.allowExternalLinks ?? true; let urlWithoutWww = baseUrl.replace("www.", ""); - let mapUrl = req.body.prompt && allowExternalLinks - ? `${req.body.prompt} ${urlWithoutWww}` - : req.body.prompt ? `${req.body.prompt} site:${urlWithoutWww}` - : `site:${urlWithoutWww}`; + let mapUrl = + req.body.prompt && allowExternalLinks + ? `${req.body.prompt} ${urlWithoutWww}` + : req.body.prompt + ? `${req.body.prompt} site:${urlWithoutWww}` + : `site:${urlWithoutWww}`; const mapResults = await getMapResults({ url: baseUrl, @@ -79,15 +81,17 @@ export async function extractController( // If we're self-hosted, we don't want to ignore the sitemap, due to our fire-engine mapping ignoreSitemap: !selfHosted ? true : false, includeMetadata: true, - includeSubdomains: req.body.includeSubdomains, + includeSubdomains: req.body.includeSubdomains }); let mappedLinks = mapResults.links as MapDocument[]; // Limit number of links to MAX_EXTRACT_LIMIT mappedLinks = mappedLinks.slice(0, MAX_EXTRACT_LIMIT); - let mappedLinksRerank = mappedLinks.map(x => `url: ${x.url}, title: ${x.title}, description: ${x.description}`); - + let mappedLinksRerank = mappedLinks.map( + (x) => `url: ${x.url}, title: ${x.title}, description: ${x.description}` + ); + // Filter by path prefix if present // wrong // if (pathPrefix) { @@ -96,32 +100,50 @@ export async function extractController( if (req.body.prompt) { // Get similarity scores between the search query and each link's context - const linksAndScores = await performRanking(mappedLinksRerank, mappedLinks.map(l => l.url), mapUrl); - + const linksAndScores = await performRanking( + mappedLinksRerank, + mappedLinks.map((l) => l.url), + mapUrl + ); + // First try with high threshold - let filteredLinks = filterAndProcessLinks(mappedLinks, linksAndScores, INITIAL_SCORE_THRESHOLD); - + let filteredLinks = filterAndProcessLinks( + mappedLinks, + linksAndScores, + INITIAL_SCORE_THRESHOLD + ); + // If we don't have enough high-quality links, try with lower threshold if (filteredLinks.length < MIN_REQUIRED_LINKS) { - logger.info(`Only found ${filteredLinks.length} links with score > ${INITIAL_SCORE_THRESHOLD}. Trying lower threshold...`); - filteredLinks = filterAndProcessLinks(mappedLinks, linksAndScores, FALLBACK_SCORE_THRESHOLD); - + logger.info( + `Only found ${filteredLinks.length} links with score > ${INITIAL_SCORE_THRESHOLD}. Trying lower threshold...` + ); + filteredLinks = filterAndProcessLinks( + mappedLinks, + linksAndScores, + FALLBACK_SCORE_THRESHOLD + ); + if (filteredLinks.length === 0) { // If still no results, take top N results regardless of score - logger.warn(`No links found with score > ${FALLBACK_SCORE_THRESHOLD}. Taking top ${MIN_REQUIRED_LINKS} results.`); + logger.warn( + `No links found with score > ${FALLBACK_SCORE_THRESHOLD}. Taking top ${MIN_REQUIRED_LINKS} results.` + ); filteredLinks = linksAndScores .sort((a, b) => b.score - a.score) .slice(0, MIN_REQUIRED_LINKS) - .map(x => mappedLinks.find(link => link.url === x.link)) - .filter((x): x is MapDocument => x !== undefined && x.url !== undefined && !isUrlBlocked(x.url)); + .map((x) => mappedLinks.find((link) => link.url === x.link)) + .filter( + (x): x is MapDocument => + x !== undefined && x.url !== undefined && !isUrlBlocked(x.url) + ); } } mappedLinks = filteredLinks.slice(0, MAX_RANKING_LIMIT); } - return mappedLinks.map(x => x.url) as string[]; - + return mappedLinks.map((x) => x.url) as string[]; } else { // Handle direct URLs without glob pattern if (!isUrlBlocked(url)) { @@ -138,7 +160,8 @@ export async function extractController( if (links.length === 0) { return res.status(400).json({ success: false, - error: "No valid URLs found to scrape. Try adjusting your search criteria or including more URLs." + error: + "No valid URLs found to scrape. Try adjusting your search criteria or including more URLs." }); } @@ -151,19 +174,19 @@ export async function extractController( const jobPriority = await getJobPriority({ plan: req.auth.plan as PlanType, team_id: req.auth.team_id, - basePriority: 10, + basePriority: 10 }); await addScrapeJob( { url, - mode: "single_urls", + mode: "single_urls", team_id: req.auth.team_id, scrapeOptions: scrapeOptions.parse({}), internalOptions: {}, plan: req.auth.plan!, origin, - is_scrape: true, + is_scrape: true }, {}, jobId, @@ -179,7 +202,10 @@ export async function extractController( return doc; } catch (e) { logger.error(`Error in scrapeController: ${e}`); - if (e instanceof Error && (e.message.startsWith("Job wait") || e.message === "timeout")) { + if ( + e instanceof Error && + (e.message.startsWith("Job wait") || e.message === "timeout") + ) { throw { status: 408, error: "Request timed out" @@ -187,7 +213,7 @@ export async function extractController( } else { throw { status: 500, - error: `(Internal server error) - ${(e && e.message) ? e.message : e}` + error: `(Internal server error) - ${e && e.message ? e.message : e}` }; } } @@ -195,7 +221,7 @@ export async function extractController( try { const results = await Promise.all(scrapePromises); - docs.push(...results.filter(doc => doc !== null).map(x => x!)); + docs.push(...results.filter((doc) => doc !== null).map((x) => x!)); } catch (e) { return res.status(e.status).json({ success: false, @@ -207,20 +233,26 @@ export async function extractController( logger.child({ method: "extractController/generateOpenAICompletions" }), { mode: "llm", - systemPrompt: "Always prioritize using the provided content to answer the question. Do not make up an answer. Be concise and follow the schema if provided. Here are the urls the user provided of which he wants to extract information from: " + links.join(", "), + systemPrompt: + "Always prioritize using the provided content to answer the question. Do not make up an answer. Be concise and follow the schema if provided. Here are the urls the user provided of which he wants to extract information from: " + + links.join(", "), prompt: req.body.prompt, - schema: req.body.schema, + schema: req.body.schema }, - docs.map(x => buildDocument(x)).join('\n'), + docs.map((x) => buildDocument(x)).join("\n"), undefined, true // isExtractEndpoint ); // TODO: change this later // While on beta, we're billing 5 credits per link discovered/scraped. - billTeam(req.auth.team_id, req.acuc?.sub_id, links.length * 5).catch(error => { - logger.error(`Failed to bill team ${req.auth.team_id} for ${links.length * 5} credits: ${error}`); - }); + billTeam(req.auth.team_id, req.acuc?.sub_id, links.length * 5).catch( + (error) => { + logger.error( + `Failed to bill team ${req.auth.team_id} for ${links.length * 5} credits: ${error}` + ); + } + ); let data = completions.extract ?? {}; let warning = completions.warning; @@ -256,12 +288,20 @@ export async function extractController( * @returns The filtered list of links. */ function filterAndProcessLinks( - mappedLinks: MapDocument[], - linksAndScores: { link: string, linkWithContext: string, score: number, originalIndex: number }[], + mappedLinks: MapDocument[], + linksAndScores: { + link: string; + linkWithContext: string; + score: number; + originalIndex: number; + }[], threshold: number ): MapDocument[] { return linksAndScores - .filter(x => x.score > threshold) - .map(x => mappedLinks.find(link => link.url === x.link)) - .filter((x): x is MapDocument => x !== undefined && x.url !== undefined && !isUrlBlocked(x.url)); -} \ No newline at end of file + .filter((x) => x.score > threshold) + .map((x) => mappedLinks.find((link) => link.url === x.link)) + .filter( + (x): x is MapDocument => + x !== undefined && x.url !== undefined && !isUrlBlocked(x.url) + ); +} diff --git a/apps/api/src/controllers/v1/map.ts b/apps/api/src/controllers/v1/map.ts index 9a0a5eb6..7ddd7b78 100644 --- a/apps/api/src/controllers/v1/map.ts +++ b/apps/api/src/controllers/v1/map.ts @@ -1,6 +1,11 @@ import { Response } from "express"; import { v4 as uuidv4 } from "uuid"; -import { MapDocument, mapRequestSchema, RequestWithAuth, scrapeOptions } from "./types"; +import { + MapDocument, + mapRequestSchema, + RequestWithAuth, + scrapeOptions +} from "./types"; import { crawlToCrawler, StoredCrawl } from "../../lib/crawl-redis"; import { MapResponse, MapRequest } from "./types"; import { configDotenv } from "dotenv"; @@ -8,7 +13,7 @@ import { checkAndUpdateURLForMap, isSameDomain, isSameSubdomain, - removeDuplicateUrls, + removeDuplicateUrls } from "../../lib/validateUrl"; import { fireEngineMap } from "../../search/fireEngine"; import { billTeam } from "../../services/billing/credit_billing"; @@ -67,13 +72,13 @@ export async function getMapResults({ crawlerOptions: { ...crawlerOptions, limit: crawlerOptions.sitemapOnly ? 10000000 : limit, - scrapeOptions: undefined, + scrapeOptions: undefined }, scrapeOptions: scrapeOptions.parse({}), internalOptions: {}, team_id: teamId, createdAt: Date.now(), - plan: plan, + plan: plan }; const crawler = crawlToCrawler(id, sc); @@ -85,7 +90,8 @@ export async function getMapResults({ sitemap.forEach((x) => { links.push(x.url); }); - links = links.slice(1) + links = links + .slice(1) .map((x) => { try { return checkAndUpdateURLForMap(x).url.trim(); @@ -99,13 +105,17 @@ export async function getMapResults({ } else { let urlWithoutWww = url.replace("www.", ""); - let mapUrl = search && allowExternalLinks - ? `${search} ${urlWithoutWww}` - : search ? `${search} site:${urlWithoutWww}` - : `site:${url}`; + let mapUrl = + search && allowExternalLinks + ? `${search} ${urlWithoutWww}` + : search + ? `${search} site:${urlWithoutWww}` + : `site:${url}`; const resultsPerPage = 100; - const maxPages = Math.ceil(Math.min(MAX_FIRE_ENGINE_RESULTS, limit) / resultsPerPage); + const maxPages = Math.ceil( + Math.min(MAX_FIRE_ENGINE_RESULTS, limit) / resultsPerPage + ); const cacheKey = `fireEngineMap:${mapUrl}`; const cachedResult = await redis.get(cacheKey); @@ -119,7 +129,7 @@ export async function getMapResults({ const fetchPage = async (page: number) => { return fireEngineMap(mapUrl, { numResults: resultsPerPage, - page: page, + page: page }); }; @@ -134,7 +144,7 @@ export async function getMapResults({ // Parallelize sitemap fetch with serper search const [sitemap, ...searchResults] = await Promise.all([ ignoreSitemap ? null : crawler.tryGetSitemap(true), - ...(cachedResult ? [] : pagePromises), + ...(cachedResult ? [] : pagePromises) ]); if (!cachedResult) { @@ -162,7 +172,7 @@ export async function getMapResults({ links = [ mapResults[0].url, ...mapResults.slice(1).map((x) => x.url), - ...links, + ...links ]; } else { mapResults.map((x) => { @@ -199,14 +209,16 @@ export async function getMapResults({ links = removeDuplicateUrls(links); } - const linksToReturn = crawlerOptions.sitemapOnly ? links : links.slice(0, limit); + const linksToReturn = crawlerOptions.sitemapOnly + ? links + : links.slice(0, limit); return { success: true, links: includeMetadata ? mapResults : linksToReturn, scrape_id: origin?.includes("website") ? id : undefined, job_id: id, - time_taken: (new Date().getTime() - Date.now()) / 1000, + time_taken: (new Date().getTime() - Date.now()) / 1000 }; } @@ -225,7 +237,7 @@ export async function mapController( crawlerOptions: req.body, origin: req.body.origin, teamId: req.auth.team_id, - plan: req.auth.plan, + plan: req.auth.plan }); // Bill the team @@ -244,12 +256,12 @@ export async function mapController( docs: result.links, time_taken: result.time_taken, team_id: req.auth.team_id, - mode: "map", + mode: "map", url: req.body.url, crawlerOptions: {}, scrapeOptions: {}, origin: req.body.origin ?? "api", - num_tokens: 0, + num_tokens: 0 }); const response = { @@ -259,4 +271,4 @@ export async function mapController( }; return res.status(200).json(response); -} \ No newline at end of file +} diff --git a/apps/api/src/controllers/v1/scrape-status.ts b/apps/api/src/controllers/v1/scrape-status.ts index b7f19a3b..b366b79e 100644 --- a/apps/api/src/controllers/v1/scrape-status.ts +++ b/apps/api/src/controllers/v1/scrape-status.ts @@ -12,30 +12,30 @@ export async function scrapeStatusController(req: any, res: any) { const job = await supabaseGetJobByIdOnlyData(req.params.jobId); const allowedTeams = [ - "41bdbfe1-0579-4d9b-b6d5-809f16be12f5", + "41bdbfe1-0579-4d9b-b6d5-809f16be12f5", "511544f2-2fce-4183-9c59-6c29b02c69b5" ]; - if(!allowedTeams.includes(job?.team_id)){ + if (!allowedTeams.includes(job?.team_id)) { return res.status(403).json({ success: false, - error: "You are not allowed to access this resource.", + error: "You are not allowed to access this resource." }); } return res.status(200).json({ success: true, - data: job?.docs[0], + data: job?.docs[0] }); } catch (error) { if (error instanceof Error && error.message == "Too Many Requests") { return res.status(429).json({ success: false, - error: "Rate limit exceeded. Please try again later.", + error: "Rate limit exceeded. Please try again later." }); } else { return res.status(500).json({ success: false, - error: "An unexpected error occurred.", + error: "An unexpected error occurred." }); } } diff --git a/apps/api/src/controllers/v1/scrape.ts b/apps/api/src/controllers/v1/scrape.ts index 9c85c91e..05cc68e3 100644 --- a/apps/api/src/controllers/v1/scrape.ts +++ b/apps/api/src/controllers/v1/scrape.ts @@ -5,7 +5,7 @@ import { RequestWithAuth, ScrapeRequest, scrapeRequestSchema, - ScrapeResponse, + ScrapeResponse } from "./types"; import { billTeam } from "../../services/billing/credit_billing"; import { v4 as uuidv4 } from "uuid"; @@ -30,7 +30,7 @@ export async function scrapeController( const jobPriority = await getJobPriority({ plan: req.auth.plan as PlanType, team_id: req.auth.team_id, - basePriority: 10, + basePriority: 10 }); await addScrapeJob( @@ -42,29 +42,37 @@ export async function scrapeController( internalOptions: {}, plan: req.auth.plan!, origin: req.body.origin, - is_scrape: true, + is_scrape: true }, {}, jobId, jobPriority ); - const totalWait = (req.body.waitFor ?? 0) + (req.body.actions ?? []).reduce((a,x) => (x.type === "wait" ? x.milliseconds ?? 0 : 0) + a, 0); + const totalWait = + (req.body.waitFor ?? 0) + + (req.body.actions ?? []).reduce( + (a, x) => (x.type === "wait" ? (x.milliseconds ?? 0) : 0) + a, + 0 + ); let doc: Document; try { doc = await waitForJob(jobId, timeout + totalWait); // TODO: better types for this } catch (e) { logger.error(`Error in scrapeController: ${e}`); - if (e instanceof Error && (e.message.startsWith("Job wait") || e.message === "timeout")) { + if ( + e instanceof Error && + (e.message.startsWith("Job wait") || e.message === "timeout") + ) { return res.status(408).json({ success: false, - error: "Request timed out", + error: "Request timed out" }); } else { return res.status(500).json({ success: false, - error: `(Internal server error) - ${(e && e.message) ? e.message : e}`, + error: `(Internal server error) - ${e && e.message ? e.message : e}` }); } } @@ -75,8 +83,8 @@ export async function scrapeController( const timeTakenInSeconds = (endTime - startTime) / 1000; const numTokens = doc && doc.extract - // ? numTokensFromString(doc.markdown, "gpt-3.5-turbo") - ? 0 // TODO: fix + ? // ? numTokensFromString(doc.markdown, "gpt-3.5-turbo") + 0 // TODO: fix : 0; let creditsToBeBilled = 1; // Assuming 1 credit per document @@ -84,14 +92,18 @@ export async function scrapeController( // Don't bill if we're early returning return; } - if(req.body.extract && req.body.formats.includes("extract")) { + if (req.body.extract && req.body.formats.includes("extract")) { creditsToBeBilled = 5; } - billTeam(req.auth.team_id, req.acuc?.sub_id, creditsToBeBilled).catch(error => { - logger.error(`Failed to bill team ${req.auth.team_id} for ${creditsToBeBilled} credits: ${error}`); - // Optionally, you could notify an admin or add to a retry queue here - }); + billTeam(req.auth.team_id, req.acuc?.sub_id, creditsToBeBilled).catch( + (error) => { + logger.error( + `Failed to bill team ${req.auth.team_id} for ${creditsToBeBilled} credits: ${error}` + ); + // Optionally, you could notify an admin or add to a retry queue here + } + ); if (!req.body.formats.includes("rawHtml")) { if (doc && doc.rawHtml) { @@ -111,12 +123,12 @@ export async function scrapeController( url: req.body.url, scrapeOptions: req.body, origin: origin, - num_tokens: numTokens, + num_tokens: numTokens }); return res.status(200).json({ success: true, data: doc, - scrape_id: origin?.includes("website") ? jobId : undefined, + scrape_id: origin?.includes("website") ? jobId : undefined }); } diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index f1596f5e..f9fa2392 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -4,7 +4,12 @@ import { isUrlBlocked } from "../../scraper/WebScraper/utils/blocklist"; import { protocolIncluded, checkUrl } from "../../lib/validateUrl"; import { PlanType } from "../../types"; import { countries } from "../../lib/validate-country"; -import { ExtractorOptions, PageOptions, ScrapeActionContent, Document as V0Document } from "../../lib/entities"; +import { + ExtractorOptions, + PageOptions, + ScrapeActionContent, + Document as V0Document +} from "../../lib/entities"; import { InternalOptions } from "../../scraper/scrapeURL"; export type Format = @@ -31,212 +36,265 @@ export const url = z.preprocess( (x) => /\.[a-z]{2,}([\/?#]|$)/i.test(x), "URL must have a valid top-level domain or be a valid path" ) - .refine( - (x) => { - try { - checkUrl(x as string) - return true; - } catch (_) { - return false; - } - }, - "Invalid URL" - ) + .refine((x) => { + try { + checkUrl(x as string); + return true; + } catch (_) { + return false; + } + }, "Invalid URL") .refine( (x) => !isUrlBlocked(x as string), "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." ) ); -const strictMessage = "Unrecognized key in body -- please review the v1 API documentation for request body changes"; +const strictMessage = + "Unrecognized key in body -- please review the v1 API documentation for request body changes"; -export const extractOptions = z.object({ - mode: z.enum(["llm"]).default("llm"), - schema: z.any().optional(), - systemPrompt: z.string().default("Based on the information on the page, extract all the information from the schema in JSON format. Try to extract all the fields even those that might not be marked as required."), - prompt: z.string().optional() -}).strict(strictMessage); +export const extractOptions = z + .object({ + mode: z.enum(["llm"]).default("llm"), + schema: z.any().optional(), + systemPrompt: z + .string() + .default( + "Based on the information on the page, extract all the information from the schema in JSON format. Try to extract all the fields even those that might not be marked as required." + ), + prompt: z.string().optional() + }) + .strict(strictMessage); export type ExtractOptions = z.infer; -export const actionsSchema = z.array(z.union([ - z.object({ - type: z.literal("wait"), - milliseconds: z.number().int().positive().finite().optional(), - selector: z.string().optional(), - }).refine( - (data) => (data.milliseconds !== undefined || data.selector !== undefined) && !(data.milliseconds !== undefined && data.selector !== undefined), - { - message: "Either 'milliseconds' or 'selector' must be provided, but not both.", - } - ), - z.object({ - type: z.literal("click"), - selector: z.string(), - }), - z.object({ - type: z.literal("screenshot"), - fullPage: z.boolean().default(false), - }), - z.object({ - type: z.literal("write"), - text: z.string(), - }), - z.object({ - type: z.literal("press"), - key: z.string(), - }), - z.object({ - type: z.literal("scroll"), - direction: z.enum(["up", "down"]).optional().default("down"), - selector: z.string().optional(), - }), - z.object({ - type: z.literal("scrape"), - }), - z.object({ - type: z.literal("executeJavascript"), - script: z.string() - }), -])); - -export const scrapeOptions = z.object({ - formats: z - .enum([ - "markdown", - "html", - "rawHtml", - "links", - "screenshot", - "screenshot@fullPage", - "extract" - ]) - .array() - .optional() - .default(["markdown"]) - .refine(x => !(x.includes("screenshot") && x.includes("screenshot@fullPage")), "You may only specify either screenshot or screenshot@fullPage"), - headers: z.record(z.string(), z.string()).optional(), - includeTags: z.string().array().optional(), - excludeTags: z.string().array().optional(), - onlyMainContent: z.boolean().default(true), - timeout: z.number().int().positive().finite().safe().optional(), - waitFor: z.number().int().nonnegative().finite().safe().default(0), - extract: extractOptions.optional(), - mobile: z.boolean().default(false), - parsePDF: z.boolean().default(true), - actions: actionsSchema.optional(), - // New - location: z.object({ - country: z.string().optional().refine( - (val) => !val || Object.keys(countries).includes(val.toUpperCase()), - { - message: "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code.", - } - ).transform(val => val ? val.toUpperCase() : 'US'), - languages: z.string().array().optional(), - }).optional(), - - // Deprecated - geolocation: z.object({ - country: z.string().optional().refine( - (val) => !val || Object.keys(countries).includes(val.toUpperCase()), - { - message: "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code.", - } - ).transform(val => val ? val.toUpperCase() : 'US'), - languages: z.string().array().optional(), - }).optional(), - skipTlsVerification: z.boolean().default(false), - removeBase64Images: z.boolean().default(true), -}).strict(strictMessage) +export const actionsSchema = z.array( + z.union([ + z + .object({ + type: z.literal("wait"), + milliseconds: z.number().int().positive().finite().optional(), + selector: z.string().optional() + }) + .refine( + (data) => + (data.milliseconds !== undefined || data.selector !== undefined) && + !(data.milliseconds !== undefined && data.selector !== undefined), + { + message: + "Either 'milliseconds' or 'selector' must be provided, but not both." + } + ), + z.object({ + type: z.literal("click"), + selector: z.string() + }), + z.object({ + type: z.literal("screenshot"), + fullPage: z.boolean().default(false) + }), + z.object({ + type: z.literal("write"), + text: z.string() + }), + z.object({ + type: z.literal("press"), + key: z.string() + }), + z.object({ + type: z.literal("scroll"), + direction: z.enum(["up", "down"]).optional().default("down"), + selector: z.string().optional() + }), + z.object({ + type: z.literal("scrape") + }), + z.object({ + type: z.literal("executeJavascript"), + script: z.string() + }) + ]) +); +export const scrapeOptions = z + .object({ + formats: z + .enum([ + "markdown", + "html", + "rawHtml", + "links", + "screenshot", + "screenshot@fullPage", + "extract" + ]) + .array() + .optional() + .default(["markdown"]) + .refine( + (x) => !(x.includes("screenshot") && x.includes("screenshot@fullPage")), + "You may only specify either screenshot or screenshot@fullPage" + ), + headers: z.record(z.string(), z.string()).optional(), + includeTags: z.string().array().optional(), + excludeTags: z.string().array().optional(), + onlyMainContent: z.boolean().default(true), + timeout: z.number().int().positive().finite().safe().optional(), + waitFor: z.number().int().nonnegative().finite().safe().default(0), + extract: extractOptions.optional(), + mobile: z.boolean().default(false), + parsePDF: z.boolean().default(true), + actions: actionsSchema.optional(), + // New + location: z + .object({ + country: z + .string() + .optional() + .refine( + (val) => !val || Object.keys(countries).includes(val.toUpperCase()), + { + message: + "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code." + } + ) + .transform((val) => (val ? val.toUpperCase() : "US")), + languages: z.string().array().optional() + }) + .optional(), + // Deprecated + geolocation: z + .object({ + country: z + .string() + .optional() + .refine( + (val) => !val || Object.keys(countries).includes(val.toUpperCase()), + { + message: + "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code." + } + ) + .transform((val) => (val ? val.toUpperCase() : "US")), + languages: z.string().array().optional() + }) + .optional(), + skipTlsVerification: z.boolean().default(false), + removeBase64Images: z.boolean().default(true) + }) + .strict(strictMessage); export type ScrapeOptions = z.infer; -export const extractV1Options = z.object({ - urls: url.array().max(10, "Maximum of 10 URLs allowed per request while in beta."), - prompt: z.string().optional(), - schema: z.any().optional(), - limit: z.number().int().positive().finite().safe().optional(), - ignoreSitemap: z.boolean().default(false), - includeSubdomains: z.boolean().default(true), - allowExternalLinks: z.boolean().default(false), - origin: z.string().optional().default("api"), - timeout: z.number().int().positive().finite().safe().default(60000) -}).strict(strictMessage) +export const extractV1Options = z + .object({ + urls: url + .array() + .max(10, "Maximum of 10 URLs allowed per request while in beta."), + prompt: z.string().optional(), + schema: z.any().optional(), + limit: z.number().int().positive().finite().safe().optional(), + ignoreSitemap: z.boolean().default(false), + includeSubdomains: z.boolean().default(true), + allowExternalLinks: z.boolean().default(false), + origin: z.string().optional().default("api"), + timeout: z.number().int().positive().finite().safe().default(60000) + }) + .strict(strictMessage); export type ExtractV1Options = z.infer; export const extractRequestSchema = extractV1Options; export type ExtractRequest = z.infer; -export const scrapeRequestSchema = scrapeOptions.omit({ timeout: true }).extend({ - url, - origin: z.string().optional().default("api"), - timeout: z.number().int().positive().finite().safe().default(30000), -}).strict(strictMessage).refine( - (obj) => { - const hasExtractFormat = obj.formats?.includes("extract"); - const hasExtractOptions = obj.extract !== undefined; - return (hasExtractFormat && hasExtractOptions) || (!hasExtractFormat && !hasExtractOptions); - }, - { - message: "When 'extract' format is specified, 'extract' options must be provided, and vice versa", - } -).transform((obj) => { - if ((obj.formats?.includes("extract") || obj.extract) && !obj.timeout) { - return { ...obj, timeout: 60000 }; - } - return obj; -}); - - +export const scrapeRequestSchema = scrapeOptions + .omit({ timeout: true }) + .extend({ + url, + origin: z.string().optional().default("api"), + timeout: z.number().int().positive().finite().safe().default(30000) + }) + .strict(strictMessage) + .refine( + (obj) => { + const hasExtractFormat = obj.formats?.includes("extract"); + const hasExtractOptions = obj.extract !== undefined; + return ( + (hasExtractFormat && hasExtractOptions) || + (!hasExtractFormat && !hasExtractOptions) + ); + }, + { + message: + "When 'extract' format is specified, 'extract' options must be provided, and vice versa" + } + ) + .transform((obj) => { + if ((obj.formats?.includes("extract") || obj.extract) && !obj.timeout) { + return { ...obj, timeout: 60000 }; + } + return obj; + }); export type ScrapeRequest = z.infer; export type ScrapeRequestInput = z.input; -export const webhookSchema = z.preprocess(x => { - if (typeof x === "string") { - return { url: x }; - } else { - return x; - } -}, z.object({ - url: z.string().url(), - headers: z.record(z.string(), z.string()).default({}), -}).strict(strictMessage)) - -export const batchScrapeRequestSchema = scrapeOptions.extend({ - urls: url.array(), - origin: z.string().optional().default("api"), - webhook: webhookSchema.optional(), - appendToId: z.string().uuid().optional(), -}).strict(strictMessage).refine( - (obj) => { - const hasExtractFormat = obj.formats?.includes("extract"); - const hasExtractOptions = obj.extract !== undefined; - return (hasExtractFormat && hasExtractOptions) || (!hasExtractFormat && !hasExtractOptions); +export const webhookSchema = z.preprocess( + (x) => { + if (typeof x === "string") { + return { url: x }; + } else { + return x; + } }, - { - message: "When 'extract' format is specified, 'extract' options must be provided, and vice versa", - } + z + .object({ + url: z.string().url(), + headers: z.record(z.string(), z.string()).default({}) + }) + .strict(strictMessage) ); +export const batchScrapeRequestSchema = scrapeOptions + .extend({ + urls: url.array(), + origin: z.string().optional().default("api"), + webhook: webhookSchema.optional(), + appendToId: z.string().uuid().optional() + }) + .strict(strictMessage) + .refine( + (obj) => { + const hasExtractFormat = obj.formats?.includes("extract"); + const hasExtractOptions = obj.extract !== undefined; + return ( + (hasExtractFormat && hasExtractOptions) || + (!hasExtractFormat && !hasExtractOptions) + ); + }, + { + message: + "When 'extract' format is specified, 'extract' options must be provided, and vice versa" + } + ); + export type BatchScrapeRequest = z.infer; -const crawlerOptions = z.object({ - includePaths: z.string().array().default([]), - excludePaths: z.string().array().default([]), - maxDepth: z.number().default(10), // default? - limit: z.number().default(10000), // default? - allowBackwardLinks: z.boolean().default(false), // >> TODO: CHANGE THIS NAME??? - allowExternalLinks: z.boolean().default(false), - allowSubdomains: z.boolean().default(false), - ignoreRobotsTxt: z.boolean().default(false), - ignoreSitemap: z.boolean().default(false), - deduplicateSimilarURLs: z.boolean().default(true), - ignoreQueryParameters: z.boolean().default(false), -}).strict(strictMessage); +const crawlerOptions = z + .object({ + includePaths: z.string().array().default([]), + excludePaths: z.string().array().default([]), + maxDepth: z.number().default(10), // default? + limit: z.number().default(10000), // default? + allowBackwardLinks: z.boolean().default(false), // >> TODO: CHANGE THIS NAME??? + allowExternalLinks: z.boolean().default(false), + allowSubdomains: z.boolean().default(false), + ignoreRobotsTxt: z.boolean().default(false), + ignoreSitemap: z.boolean().default(false), + deduplicateSimilarURLs: z.boolean().default(true), + ignoreQueryParameters: z.boolean().default(false) + }) + .strict(strictMessage); // export type CrawlerOptions = { // includePaths?: string[]; @@ -250,13 +308,15 @@ const crawlerOptions = z.object({ export type CrawlerOptions = z.infer; -export const crawlRequestSchema = crawlerOptions.extend({ - url, - origin: z.string().optional().default("api"), - scrapeOptions: scrapeOptions.default({}), - webhook: webhookSchema.optional(), - limit: z.number().default(10000), -}).strict(strictMessage); +export const crawlRequestSchema = crawlerOptions + .extend({ + url, + origin: z.string().optional().default("api"), + scrapeOptions: scrapeOptions.default({}), + webhook: webhookSchema.optional(), + limit: z.number().default(10000) + }) + .strict(strictMessage); // export type CrawlRequest = { // url: string; @@ -270,18 +330,19 @@ export const crawlRequestSchema = crawlerOptions.extend({ // extractionSchema?: Record; // } - export type CrawlRequest = z.infer; -export const mapRequestSchema = crawlerOptions.extend({ - url, - origin: z.string().optional().default("api"), - includeSubdomains: z.boolean().default(true), - search: z.string().optional(), - ignoreSitemap: z.boolean().default(false), - sitemapOnly: z.boolean().default(false), - limit: z.number().min(1).max(5000).default(5000), -}).strict(strictMessage); +export const mapRequestSchema = crawlerOptions + .extend({ + url, + origin: z.string().optional().default("api"), + includeSubdomains: z.boolean().default(true), + search: z.string().optional(), + ignoreSitemap: z.boolean().default(false), + sitemapOnly: z.boolean().default(false), + limit: z.number().min(1).max(5000).default(5000) + }) + .strict(strictMessage); // export type MapRequest = { // url: string; @@ -451,7 +512,7 @@ export interface RequestWithMaybeACUC< ReqBody = undefined, ResBody = undefined > extends Request { - acuc?: AuthCreditUsageChunk, + acuc?: AuthCreditUsageChunk; } export interface RequestWithACUC< @@ -459,13 +520,13 @@ export interface RequestWithACUC< ReqBody = undefined, ResBody = undefined > extends Request { - acuc: AuthCreditUsageChunk, + acuc: AuthCreditUsageChunk; } export interface RequestWithAuth< ReqParams = {}, ReqBody = undefined, - ResBody = undefined, + ResBody = undefined > extends Request { auth: AuthObject; account?: Account; @@ -483,16 +544,15 @@ export interface RequestWithMaybeAuth< export interface RequestWithAuth< ReqParams = {}, ReqBody = undefined, - ResBody = undefined, + ResBody = undefined > extends RequestWithACUC { auth: AuthObject; account?: Account; } -export interface ResponseWithSentry< - ResBody = undefined, -> extends Response { - sentry?: string, +export interface ResponseWithSentry + extends Response { + sentry?: string; } export function toLegacyCrawlerOptions(x: CrawlerOptions) { @@ -509,11 +569,14 @@ export function toLegacyCrawlerOptions(x: CrawlerOptions) { ignoreRobotsTxt: x.ignoreRobotsTxt, ignoreSitemap: x.ignoreSitemap, deduplicateSimilarURLs: x.deduplicateSimilarURLs, - ignoreQueryParameters: x.ignoreQueryParameters, + ignoreQueryParameters: x.ignoreQueryParameters }; } -export function fromLegacyCrawlerOptions(x: any): { crawlOptions: CrawlerOptions; internalOptions: InternalOptions } { +export function fromLegacyCrawlerOptions(x: any): { + crawlOptions: CrawlerOptions; + internalOptions: InternalOptions; +} { return { crawlOptions: crawlerOptions.parse({ includePaths: x.includes, @@ -526,37 +589,50 @@ export function fromLegacyCrawlerOptions(x: any): { crawlOptions: CrawlerOptions ignoreRobotsTxt: x.ignoreRobotsTxt, ignoreSitemap: x.ignoreSitemap, deduplicateSimilarURLs: x.deduplicateSimilarURLs, - ignoreQueryParameters: x.ignoreQueryParameters, + ignoreQueryParameters: x.ignoreQueryParameters }), internalOptions: { - v0CrawlOnlyUrls: x.returnOnlyUrls, - }, + v0CrawlOnlyUrls: x.returnOnlyUrls + } }; } - - export interface MapDocument { url: string; title?: string; description?: string; -} -export function fromLegacyScrapeOptions(pageOptions: PageOptions, extractorOptions: ExtractorOptions | undefined, timeout: number | undefined): { scrapeOptions: ScrapeOptions, internalOptions: InternalOptions } { +} +export function fromLegacyScrapeOptions( + pageOptions: PageOptions, + extractorOptions: ExtractorOptions | undefined, + timeout: number | undefined +): { scrapeOptions: ScrapeOptions; internalOptions: InternalOptions } { return { scrapeOptions: scrapeOptions.parse({ formats: [ - (pageOptions.includeMarkdown ?? true) ? "markdown" as const : null, - (pageOptions.includeHtml ?? false) ? "html" as const : null, - (pageOptions.includeRawHtml ?? false) ? "rawHtml" as const : null, - (pageOptions.screenshot ?? false) ? "screenshot" as const : null, - (pageOptions.fullPageScreenshot ?? false) ? "screenshot@fullPage" as const : null, - (extractorOptions !== undefined && extractorOptions.mode.includes("llm-extraction")) ? "extract" as const : null, + (pageOptions.includeMarkdown ?? true) ? ("markdown" as const) : null, + (pageOptions.includeHtml ?? false) ? ("html" as const) : null, + (pageOptions.includeRawHtml ?? false) ? ("rawHtml" as const) : null, + (pageOptions.screenshot ?? false) ? ("screenshot" as const) : null, + (pageOptions.fullPageScreenshot ?? false) + ? ("screenshot@fullPage" as const) + : null, + extractorOptions !== undefined && + extractorOptions.mode.includes("llm-extraction") + ? ("extract" as const) + : null, "links" - ].filter(x => x !== null), + ].filter((x) => x !== null), waitFor: pageOptions.waitFor, headers: pageOptions.headers, - includeTags: (typeof pageOptions.onlyIncludeTags === "string" ? [pageOptions.onlyIncludeTags] : pageOptions.onlyIncludeTags), - excludeTags: (typeof pageOptions.removeTags === "string" ? [pageOptions.removeTags] : pageOptions.removeTags), + includeTags: + typeof pageOptions.onlyIncludeTags === "string" + ? [pageOptions.onlyIncludeTags] + : pageOptions.onlyIncludeTags, + excludeTags: + typeof pageOptions.removeTags === "string" + ? [pageOptions.removeTags] + : pageOptions.removeTags, onlyMainContent: pageOptions.onlyMainContent ?? false, timeout: timeout, parsePDF: pageOptions.parsePDF, @@ -564,29 +640,45 @@ export function fromLegacyScrapeOptions(pageOptions: PageOptions, extractorOptio location: pageOptions.geolocation, skipTlsVerification: pageOptions.skipTlsVerification, removeBase64Images: pageOptions.removeBase64Images, - extract: extractorOptions !== undefined && extractorOptions.mode.includes("llm-extraction") ? { - systemPrompt: extractorOptions.extractionPrompt, - prompt: extractorOptions.userPrompt, - schema: extractorOptions.extractionSchema, - } : undefined, - mobile: pageOptions.mobile, + extract: + extractorOptions !== undefined && + extractorOptions.mode.includes("llm-extraction") + ? { + systemPrompt: extractorOptions.extractionPrompt, + prompt: extractorOptions.userPrompt, + schema: extractorOptions.extractionSchema + } + : undefined, + mobile: pageOptions.mobile }), internalOptions: { atsv: pageOptions.atsv, v0DisableJsDom: pageOptions.disableJsDom, - v0UseFastMode: pageOptions.useFastMode, - }, + v0UseFastMode: pageOptions.useFastMode + } // TODO: fallback, fetchPageContent, replaceAllPathsWithAbsolutePaths, includeLinks - } + }; } -export function fromLegacyCombo(pageOptions: PageOptions, extractorOptions: ExtractorOptions | undefined, timeout: number | undefined, crawlerOptions: any): { scrapeOptions: ScrapeOptions, internalOptions: InternalOptions} { - const { scrapeOptions, internalOptions: i1 } = fromLegacyScrapeOptions(pageOptions, extractorOptions, timeout); +export function fromLegacyCombo( + pageOptions: PageOptions, + extractorOptions: ExtractorOptions | undefined, + timeout: number | undefined, + crawlerOptions: any +): { scrapeOptions: ScrapeOptions; internalOptions: InternalOptions } { + const { scrapeOptions, internalOptions: i1 } = fromLegacyScrapeOptions( + pageOptions, + extractorOptions, + timeout + ); const { internalOptions: i2 } = fromLegacyCrawlerOptions(crawlerOptions); return { scrapeOptions, internalOptions: Object.assign(i1, i2) }; } -export function toLegacyDocument(document: Document, internalOptions: InternalOptions): V0Document | { url: string; } { +export function toLegacyDocument( + document: Document, + internalOptions: InternalOptions +): V0Document | { url: string } { if (internalOptions.v0CrawlOnlyUrls) { return { url: document.metadata.sourceURL! }; } @@ -604,9 +696,9 @@ export function toLegacyDocument(document: Document, internalOptions: InternalOp statusCode: undefined, pageError: document.metadata.error, pageStatusCode: document.metadata.statusCode, - screenshot: document.screenshot, + screenshot: document.screenshot }, - actions: document.actions , - warning: document.warning, - } + actions: document.actions, + warning: document.warning + }; } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e32bf97f..905c32d8 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import "./services/sentry" +import "./services/sentry"; import * as Sentry from "@sentry/node"; import express, { NextFunction, Request, Response } from "express"; import bodyParser from "body-parser"; @@ -9,9 +9,9 @@ import { v0Router } from "./routes/v0"; import os from "os"; import { logger } from "./lib/logger"; import { adminRouter } from "./routes/admin"; -import http from 'node:http'; -import https from 'node:https'; -import CacheableLookup from 'cacheable-lookup'; +import http from "node:http"; +import https from "node:https"; +import CacheableLookup from "cacheable-lookup"; import { v1Router } from "./routes/v1"; import expressWs from "express-ws"; import { ErrorResponse, ResponseWithSentry } from "./controllers/v1/types"; @@ -25,14 +25,12 @@ const { ExpressAdapter } = require("@bull-board/express"); const numCPUs = process.env.ENV === "local" ? 2 : os.cpus().length; logger.info(`Number of CPUs: ${numCPUs} available`); -const cacheable = new CacheableLookup() - +const cacheable = new CacheableLookup(); // Install cacheable lookup for all other requests cacheable.install(http.globalAgent); cacheable.install(https.globalAgent); - const ws = expressWs(express()); const app = ws.app; @@ -48,7 +46,7 @@ serverAdapter.setBasePath(`/admin/${process.env.BULL_AUTH_KEY}/queues`); const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard({ queues: [new BullAdapter(getScrapeQueue())], - serverAdapter: serverAdapter, + serverAdapter: serverAdapter }); app.use( @@ -82,15 +80,15 @@ function startServer(port = DEFAULT_PORT) { }); const exitHandler = () => { - logger.info('SIGTERM signal received: closing HTTP server') + logger.info("SIGTERM signal received: closing HTTP server"); server.close(() => { logger.info("Server closed."); process.exit(0); }); }; - process.on('SIGTERM', exitHandler); - process.on('SIGINT', exitHandler); + process.on("SIGTERM", exitHandler); + process.on("SIGINT", exitHandler); return server; } @@ -101,13 +99,11 @@ if (require.main === module) { app.get(`/serverHealthCheck`, async (req, res) => { try { const scrapeQueue = getScrapeQueue(); - const [waitingJobs] = await Promise.all([ - scrapeQueue.getWaitingCount(), - ]); + const [waitingJobs] = await Promise.all([scrapeQueue.getWaitingCount()]); const noWaitingJobs = waitingJobs === 0; // 200 if no active jobs, 503 if there are active jobs return res.status(noWaitingJobs ? 200 : 500).json({ - waitingJobs, + waitingJobs }); } catch (error) { Sentry.captureException(error); @@ -124,7 +120,7 @@ app.get("/serverHealthCheck/notify", async (req, res) => { const getWaitingJobsCount = async () => { const scrapeQueue = getScrapeQueue(); const [waitingJobsCount] = await Promise.all([ - scrapeQueue.getWaitingCount(), + scrapeQueue.getWaitingCount() ]); return waitingJobsCount; @@ -144,15 +140,15 @@ app.get("/serverHealthCheck/notify", async (req, res) => { const message = { text: `⚠️ Warning: The number of active jobs (${waitingJobsCount}) has exceeded the threshold (${treshold}) for more than ${ timeout / 60000 - } minute(s).`, + } minute(s).` }; const response = await fetch(slackWebhookUrl, { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json" }, - body: JSON.stringify(message), + body: JSON.stringify(message) }); if (!response.ok) { @@ -175,40 +171,80 @@ app.get("/is-production", (req, res) => { res.send({ isProduction: global.isProduction }); }); -app.use((err: unknown, req: Request<{}, ErrorResponse, undefined>, res: Response, next: NextFunction) => { - if (err instanceof ZodError) { - if (Array.isArray(err.errors) && err.errors.find(x => x.message === "URL uses unsupported protocol")) { +app.use( + ( + err: unknown, + req: Request<{}, ErrorResponse, undefined>, + res: Response, + next: NextFunction + ) => { + if (err instanceof ZodError) { + if ( + Array.isArray(err.errors) && + err.errors.find((x) => x.message === "URL uses unsupported protocol") + ) { logger.warn("Unsupported protocol error: " + JSON.stringify(req.body)); } - res.status(400).json({ success: false, error: "Bad Request", details: err.errors }); - } else { + res + .status(400) + .json({ success: false, error: "Bad Request", details: err.errors }); + } else { next(err); + } } -}); +); Sentry.setupExpressErrorHandler(app); -app.use((err: unknown, req: Request<{}, ErrorResponse, undefined>, res: ResponseWithSentry, next: NextFunction) => { - if (err instanceof SyntaxError && 'status' in err && err.status === 400 && 'body' in err) { - return res.status(400).json({ success: false, error: 'Bad request, malformed JSON' }); - } - - const id = res.sentry ?? uuidv4(); - let verbose = JSON.stringify(err); - if (verbose === "{}") { - if (err instanceof Error) { - verbose = JSON.stringify({ - message: err.message, - name: err.name, - stack: err.stack, - }); +app.use( + ( + err: unknown, + req: Request<{}, ErrorResponse, undefined>, + res: ResponseWithSentry, + next: NextFunction + ) => { + if ( + err instanceof SyntaxError && + "status" in err && + err.status === 400 && + "body" in err + ) { + return res + .status(400) + .json({ success: false, error: "Bad request, malformed JSON" }); } - } - logger.error("Error occurred in request! (" + req.path + ") -- ID " + id + " -- " + verbose); - res.status(500).json({ success: false, error: "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + id }); -}); + const id = res.sentry ?? uuidv4(); + let verbose = JSON.stringify(err); + if (verbose === "{}") { + if (err instanceof Error) { + verbose = JSON.stringify({ + message: err.message, + name: err.name, + stack: err.stack + }); + } + } + + logger.error( + "Error occurred in request! (" + + req.path + + ") -- ID " + + id + + " -- " + + verbose + ); + res + .status(500) + .json({ + success: false, + error: + "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + + id + }); + } +); logger.info(`Worker ${process.pid} started`); @@ -220,6 +256,3 @@ logger.info(`Worker ${process.pid} started`); // sq.on("paused", j => ScrapeEvents.logJobEvent(j, "paused")); // sq.on("resumed", j => ScrapeEvents.logJobEvent(j, "resumed")); // sq.on("removed", j => ScrapeEvents.logJobEvent(j, "removed")); - - - diff --git a/apps/api/src/lib/LLM-extraction/index.ts b/apps/api/src/lib/LLM-extraction/index.ts index 430dc1d4..47ecaf18 100644 --- a/apps/api/src/lib/LLM-extraction/index.ts +++ b/apps/api/src/lib/LLM-extraction/index.ts @@ -32,7 +32,7 @@ export async function generateCompletions( schema: schema, prompt: prompt, systemPrompt: systemPrompt, - mode: mode, + mode: mode }); // Validate the JSON output against the schema using AJV if (schema) { diff --git a/apps/api/src/lib/LLM-extraction/models.ts b/apps/api/src/lib/LLM-extraction/models.ts index f777dce9..563863c0 100644 --- a/apps/api/src/lib/LLM-extraction/models.ts +++ b/apps/api/src/lib/LLM-extraction/models.ts @@ -50,7 +50,7 @@ export async function generateOpenAICompletions({ systemPrompt = defaultPrompt, prompt, temperature, - mode, + mode }: { client: OpenAI; model?: string; @@ -68,7 +68,7 @@ export async function generateOpenAICompletions({ return { ...document, warning: - "LLM extraction was not performed since the document's content is empty or missing.", + "LLM extraction was not performed since the document's content is empty or missing." }; } const [content, numTokens] = preparedDoc; @@ -81,16 +81,16 @@ export async function generateOpenAICompletions({ messages: [ { role: "system", - content: systemPrompt, + content: systemPrompt }, { role: "user", content }, { role: "user", - content: `Transform the above content into structured json output based on the following user request: ${prompt}`, - }, + content: `Transform the above content into structured json output based on the following user request: ${prompt}` + } ], response_format: { type: "json_object" }, - temperature, + temperature }); try { @@ -106,9 +106,9 @@ export async function generateOpenAICompletions({ messages: [ { role: "system", - content: systemPrompt, + content: systemPrompt }, - { role: "user", content }, + { role: "user", content } ], tools: [ { @@ -116,12 +116,12 @@ export async function generateOpenAICompletions({ function: { name: "extract_content", description: "Extracts the content from the given webpage(s)", - parameters: schema, - }, - }, + parameters: schema + } + } ], tool_choice: { type: "function", function: { name: "extract_content" } }, - temperature, + temperature }); const c = completion.choices[0].message.tool_calls[0].function.arguments; @@ -140,6 +140,6 @@ export async function generateOpenAICompletions({ warning: numTokens > maxTokens ? `Page was trimmed to fit the maximum token limit defined by the LLM model (Max: ${maxTokens} tokens, Attemped: ${numTokens} tokens). If results are not good, email us at help@mendable.ai so we can help you.` - : undefined, + : undefined }; } diff --git a/apps/api/src/lib/__tests__/html-to-markdown.test.ts b/apps/api/src/lib/__tests__/html-to-markdown.test.ts index 3c68c959..f69c2949 100644 --- a/apps/api/src/lib/__tests__/html-to-markdown.test.ts +++ b/apps/api/src/lib/__tests__/html-to-markdown.test.ts @@ -1,36 +1,46 @@ -import { parseMarkdown } from '../html-to-markdown'; +import { parseMarkdown } from "../html-to-markdown"; -describe('parseMarkdown', () => { - it('should correctly convert simple HTML to Markdown', async () => { - const html = '

Hello, world!

'; - const expectedMarkdown = 'Hello, world!'; +describe("parseMarkdown", () => { + it("should correctly convert simple HTML to Markdown", async () => { + const html = "

Hello, world!

"; + const expectedMarkdown = "Hello, world!"; await expect(parseMarkdown(html)).resolves.toBe(expectedMarkdown); }); - it('should convert complex HTML with nested elements to Markdown', async () => { - const html = '

Hello bold world!

  • List item
'; - const expectedMarkdown = 'Hello **bold** world!\n\n- List item'; + it("should convert complex HTML with nested elements to Markdown", async () => { + const html = + "

Hello bold world!

  • List item
"; + const expectedMarkdown = "Hello **bold** world!\n\n- List item"; await expect(parseMarkdown(html)).resolves.toBe(expectedMarkdown); }); - it('should return empty string when input is empty', async () => { - const html = ''; - const expectedMarkdown = ''; + it("should return empty string when input is empty", async () => { + const html = ""; + const expectedMarkdown = ""; await expect(parseMarkdown(html)).resolves.toBe(expectedMarkdown); }); - it('should handle null input gracefully', async () => { + it("should handle null input gracefully", async () => { const html = null; - const expectedMarkdown = ''; + const expectedMarkdown = ""; await expect(parseMarkdown(html)).resolves.toBe(expectedMarkdown); }); - it('should handle various types of invalid HTML gracefully', async () => { + it("should handle various types of invalid HTML gracefully", async () => { const invalidHtmls = [ - { html: '

Unclosed tag', expected: 'Unclosed tag' }, - { html: '

Missing closing div', expected: 'Missing closing div' }, - { html: '

Wrong nesting

', expected: '**Wrong nesting**' }, - { html: '
Link without closing tag', expected: '[Link without closing tag](http://example.com)' } + { html: "

Unclosed tag", expected: "Unclosed tag" }, + { + html: "

Missing closing div", + expected: "Missing closing div" + }, + { + html: "

Wrong nesting

", + expected: "**Wrong nesting**" + }, + { + html: '
Link without closing tag', + expected: "[Link without closing tag](http://example.com)" + } ]; for (const { html, expected } of invalidHtmls) { diff --git a/apps/api/src/lib/__tests__/job-priority.test.ts b/apps/api/src/lib/__tests__/job-priority.test.ts index 82477379..4bd5fda9 100644 --- a/apps/api/src/lib/__tests__/job-priority.test.ts +++ b/apps/api/src/lib/__tests__/job-priority.test.ts @@ -1,7 +1,7 @@ import { getJobPriority, addJobPriority, - deleteJobPriority, + deleteJobPriority } from "../job-priority"; import { redisConnection } from "../../services/queue-service"; import { PlanType } from "../../types"; @@ -11,8 +11,8 @@ jest.mock("../../services/queue-service", () => ({ sadd: jest.fn(), srem: jest.fn(), scard: jest.fn(), - expire: jest.fn(), - }, + expire: jest.fn() + } })); describe("Job Priority Tests", () => { diff --git a/apps/api/src/lib/batch-process.ts b/apps/api/src/lib/batch-process.ts index 802d1eb1..20bb4ab6 100644 --- a/apps/api/src/lib/batch-process.ts +++ b/apps/api/src/lib/batch-process.ts @@ -1,16 +1,15 @@ export async function batchProcess( - array: T[], - batchSize: number, - asyncFunction: (item: T, index: number) => Promise - ): Promise { - const batches: T[][] = []; - for (let i = 0; i < array.length; i += batchSize) { - const batch = array.slice(i, i + batchSize); - batches.push(batch); - } - - for (const batch of batches) { - await Promise.all(batch.map((item, i) => asyncFunction(item, i))); - } + array: T[], + batchSize: number, + asyncFunction: (item: T, index: number) => Promise +): Promise { + const batches: T[][] = []; + for (let i = 0; i < array.length; i += batchSize) { + const batch = array.slice(i, i + batchSize); + batches.push(batch); } - \ No newline at end of file + + for (const batch of batches) { + await Promise.all(batch.map((item, i) => asyncFunction(item, i))); + } +} diff --git a/apps/api/src/lib/cache.ts b/apps/api/src/lib/cache.ts index 896d9429..30c9f0b4 100644 --- a/apps/api/src/lib/cache.ts +++ b/apps/api/src/lib/cache.ts @@ -2,49 +2,61 @@ import IORedis from "ioredis"; import { ScrapeOptions } from "../controllers/v1/types"; import { InternalOptions } from "../scraper/scrapeURL"; import { logger as _logger } from "./logger"; -const logger = _logger.child({module: "cache"}); +const logger = _logger.child({ module: "cache" }); -export const cacheRedis = process.env.CACHE_REDIS_URL ? new IORedis(process.env.CACHE_REDIS_URL, { - maxRetriesPerRequest: null, -}) : null; +export const cacheRedis = process.env.CACHE_REDIS_URL + ? new IORedis(process.env.CACHE_REDIS_URL, { + maxRetriesPerRequest: null + }) + : null; -export function cacheKey(url: string, scrapeOptions: ScrapeOptions, internalOptions: InternalOptions): string | null { - if (!cacheRedis) return null; +export function cacheKey( + url: string, + scrapeOptions: ScrapeOptions, + internalOptions: InternalOptions +): string | null { + if (!cacheRedis) return null; - // these options disqualify a cache - if (internalOptions.v0CrawlOnlyUrls || internalOptions.forceEngine || internalOptions.v0UseFastMode || internalOptions.atsv - || (scrapeOptions.actions && scrapeOptions.actions.length > 0) - ) { - return null; - } + // these options disqualify a cache + if ( + internalOptions.v0CrawlOnlyUrls || + internalOptions.forceEngine || + internalOptions.v0UseFastMode || + internalOptions.atsv || + (scrapeOptions.actions && scrapeOptions.actions.length > 0) + ) { + return null; + } - return "cache:" + url + ":waitFor:" + scrapeOptions.waitFor; + return "cache:" + url + ":waitFor:" + scrapeOptions.waitFor; } export type CacheEntry = { - url: string; - html: string; - statusCode: number; - error?: string; + url: string; + html: string; + statusCode: number; + error?: string; }; export async function saveEntryToCache(key: string, entry: CacheEntry) { - if (!cacheRedis) return; + if (!cacheRedis) return; - try { - await cacheRedis.set(key, JSON.stringify(entry)); - } catch (error) { - logger.warn("Failed to save to cache", { key, error }); - } + try { + await cacheRedis.set(key, JSON.stringify(entry)); + } catch (error) { + logger.warn("Failed to save to cache", { key, error }); + } } -export async function getEntryFromCache(key: string): Promise { - if (!cacheRedis) return null; +export async function getEntryFromCache( + key: string +): Promise { + if (!cacheRedis) return null; - try { - return JSON.parse(await cacheRedis.get(key) ?? "null"); - } catch (error) { - logger.warn("Failed to get from cache", { key, error }); - return null; - } + try { + return JSON.parse((await cacheRedis.get(key)) ?? "null"); + } catch (error) { + logger.warn("Failed to get from cache", { key, error }); + return null; + } } diff --git a/apps/api/src/lib/concurrency-limit.ts b/apps/api/src/lib/concurrency-limit.ts index 72dc1e45..aba1fd3a 100644 --- a/apps/api/src/lib/concurrency-limit.ts +++ b/apps/api/src/lib/concurrency-limit.ts @@ -4,45 +4,76 @@ import { RateLimiterMode } from "../types"; import { JobsOptions } from "bullmq"; const constructKey = (team_id: string) => "concurrency-limiter:" + team_id; -const constructQueueKey = (team_id: string) => "concurrency-limit-queue:" + team_id; +const constructQueueKey = (team_id: string) => + "concurrency-limit-queue:" + team_id; const stalledJobTimeoutMs = 2 * 60 * 1000; export function getConcurrencyLimitMax(plan: string): number { - return getRateLimiterPoints(RateLimiterMode.Scrape, undefined, plan); + return getRateLimiterPoints(RateLimiterMode.Scrape, undefined, plan); } -export async function cleanOldConcurrencyLimitEntries(team_id: string, now: number = Date.now()) { - await redisConnection.zremrangebyscore(constructKey(team_id), -Infinity, now); +export async function cleanOldConcurrencyLimitEntries( + team_id: string, + now: number = Date.now() +) { + await redisConnection.zremrangebyscore(constructKey(team_id), -Infinity, now); } -export async function getConcurrencyLimitActiveJobs(team_id: string, now: number = Date.now()): Promise { - return await redisConnection.zrangebyscore(constructKey(team_id), now, Infinity); +export async function getConcurrencyLimitActiveJobs( + team_id: string, + now: number = Date.now() +): Promise { + return await redisConnection.zrangebyscore( + constructKey(team_id), + now, + Infinity + ); } -export async function pushConcurrencyLimitActiveJob(team_id: string, id: string, now: number = Date.now()) { - await redisConnection.zadd(constructKey(team_id), now + stalledJobTimeoutMs, id); +export async function pushConcurrencyLimitActiveJob( + team_id: string, + id: string, + now: number = Date.now() +) { + await redisConnection.zadd( + constructKey(team_id), + now + stalledJobTimeoutMs, + id + ); } -export async function removeConcurrencyLimitActiveJob(team_id: string, id: string) { - await redisConnection.zrem(constructKey(team_id), id); +export async function removeConcurrencyLimitActiveJob( + team_id: string, + id: string +) { + await redisConnection.zrem(constructKey(team_id), id); } export type ConcurrencyLimitedJob = { - id: string; - data: any; - opts: JobsOptions; - priority?: number; + id: string; + data: any; + opts: JobsOptions; + priority?: number; +}; + +export async function takeConcurrencyLimitedJob( + team_id: string +): Promise { + const res = await redisConnection.zmpop(1, constructQueueKey(team_id), "MIN"); + if (res === null || res === undefined) { + return null; + } + + return JSON.parse(res[1][0][0]); } -export async function takeConcurrencyLimitedJob(team_id: string): Promise { - const res = await redisConnection.zmpop(1, constructQueueKey(team_id), "MIN"); - if (res === null || res === undefined) { - return null; - } - - return JSON.parse(res[1][0][0]); -} - -export async function pushConcurrencyLimitedJob(team_id: string, job: ConcurrencyLimitedJob) { - await redisConnection.zadd(constructQueueKey(team_id), job.priority ?? 1, JSON.stringify(job)); +export async function pushConcurrencyLimitedJob( + team_id: string, + job: ConcurrencyLimitedJob +) { + await redisConnection.zadd( + constructQueueKey(team_id), + job.priority ?? 1, + JSON.stringify(job) + ); } diff --git a/apps/api/src/lib/crawl-redis.test.ts b/apps/api/src/lib/crawl-redis.test.ts index eb9c81f1..ef2dabee 100644 --- a/apps/api/src/lib/crawl-redis.test.ts +++ b/apps/api/src/lib/crawl-redis.test.ts @@ -1,33 +1,41 @@ import { generateURLPermutations } from "./crawl-redis"; describe("generateURLPermutations", () => { - it("generates permutations correctly", () => { - const bareHttps = generateURLPermutations("https://firecrawl.dev").map(x => x.href); - expect(bareHttps.length).toBe(4); - expect(bareHttps.includes("https://firecrawl.dev/")).toBe(true); - expect(bareHttps.includes("https://www.firecrawl.dev/")).toBe(true); - expect(bareHttps.includes("http://firecrawl.dev/")).toBe(true); - expect(bareHttps.includes("http://www.firecrawl.dev/")).toBe(true); + it("generates permutations correctly", () => { + const bareHttps = generateURLPermutations("https://firecrawl.dev").map( + (x) => x.href + ); + expect(bareHttps.length).toBe(4); + expect(bareHttps.includes("https://firecrawl.dev/")).toBe(true); + expect(bareHttps.includes("https://www.firecrawl.dev/")).toBe(true); + expect(bareHttps.includes("http://firecrawl.dev/")).toBe(true); + expect(bareHttps.includes("http://www.firecrawl.dev/")).toBe(true); - const bareHttp = generateURLPermutations("http://firecrawl.dev").map(x => x.href); - expect(bareHttp.length).toBe(4); - expect(bareHttp.includes("https://firecrawl.dev/")).toBe(true); - expect(bareHttp.includes("https://www.firecrawl.dev/")).toBe(true); - expect(bareHttp.includes("http://firecrawl.dev/")).toBe(true); - expect(bareHttp.includes("http://www.firecrawl.dev/")).toBe(true); + const bareHttp = generateURLPermutations("http://firecrawl.dev").map( + (x) => x.href + ); + expect(bareHttp.length).toBe(4); + expect(bareHttp.includes("https://firecrawl.dev/")).toBe(true); + expect(bareHttp.includes("https://www.firecrawl.dev/")).toBe(true); + expect(bareHttp.includes("http://firecrawl.dev/")).toBe(true); + expect(bareHttp.includes("http://www.firecrawl.dev/")).toBe(true); - const wwwHttps = generateURLPermutations("https://www.firecrawl.dev").map(x => x.href); - expect(wwwHttps.length).toBe(4); - expect(wwwHttps.includes("https://firecrawl.dev/")).toBe(true); - expect(wwwHttps.includes("https://www.firecrawl.dev/")).toBe(true); - expect(wwwHttps.includes("http://firecrawl.dev/")).toBe(true); - expect(wwwHttps.includes("http://www.firecrawl.dev/")).toBe(true); + const wwwHttps = generateURLPermutations("https://www.firecrawl.dev").map( + (x) => x.href + ); + expect(wwwHttps.length).toBe(4); + expect(wwwHttps.includes("https://firecrawl.dev/")).toBe(true); + expect(wwwHttps.includes("https://www.firecrawl.dev/")).toBe(true); + expect(wwwHttps.includes("http://firecrawl.dev/")).toBe(true); + expect(wwwHttps.includes("http://www.firecrawl.dev/")).toBe(true); - const wwwHttp = generateURLPermutations("http://www.firecrawl.dev").map(x => x.href); - expect(wwwHttp.length).toBe(4); - expect(wwwHttp.includes("https://firecrawl.dev/")).toBe(true); - expect(wwwHttp.includes("https://www.firecrawl.dev/")).toBe(true); - expect(wwwHttp.includes("http://firecrawl.dev/")).toBe(true); - expect(wwwHttp.includes("http://www.firecrawl.dev/")).toBe(true); - }) -}); \ No newline at end of file + const wwwHttp = generateURLPermutations("http://www.firecrawl.dev").map( + (x) => x.href + ); + expect(wwwHttp.length).toBe(4); + expect(wwwHttp.includes("https://firecrawl.dev/")).toBe(true); + expect(wwwHttp.includes("https://www.firecrawl.dev/")).toBe(true); + expect(wwwHttp.includes("http://firecrawl.dev/")).toBe(true); + expect(wwwHttp.includes("http://www.firecrawl.dev/")).toBe(true); + }); +}); diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index 842f6ebf..ab1a238d 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -6,222 +6,331 @@ import { logger as _logger } from "./logger"; import { getAdjustedMaxDepth } from "../scraper/WebScraper/utils/maxDepthUtils"; export type StoredCrawl = { - originUrl?: string; - crawlerOptions: any; - scrapeOptions: Omit; - internalOptions: InternalOptions; - team_id: string; - plan?: string; - robots?: string; - cancelled?: boolean; - createdAt: number; + originUrl?: string; + crawlerOptions: any; + scrapeOptions: Omit; + internalOptions: InternalOptions; + team_id: string; + plan?: string; + robots?: string; + cancelled?: boolean; + createdAt: number; }; export async function saveCrawl(id: string, crawl: StoredCrawl) { - _logger.debug("Saving crawl " + id + " to Redis...", { crawl, module: "crawl-redis", method: "saveCrawl", crawlId: id, teamId: crawl.team_id, plan: crawl.plan }); - await redisConnection.set("crawl:" + id, JSON.stringify(crawl)); - await redisConnection.expire("crawl:" + id, 24 * 60 * 60, "NX"); + _logger.debug("Saving crawl " + id + " to Redis...", { + crawl, + module: "crawl-redis", + method: "saveCrawl", + crawlId: id, + teamId: crawl.team_id, + plan: crawl.plan + }); + await redisConnection.set("crawl:" + id, JSON.stringify(crawl)); + await redisConnection.expire("crawl:" + id, 24 * 60 * 60, "NX"); } export async function getCrawl(id: string): Promise { - const x = await redisConnection.get("crawl:" + id); + const x = await redisConnection.get("crawl:" + id); - if (x === null) { - return null; - } + if (x === null) { + return null; + } - return JSON.parse(x); + return JSON.parse(x); } export async function getCrawlExpiry(id: string): Promise { - const d = new Date(); - const ttl = await redisConnection.pttl("crawl:" + id); - d.setMilliseconds(d.getMilliseconds() + ttl); - d.setMilliseconds(0); - return d; + const d = new Date(); + const ttl = await redisConnection.pttl("crawl:" + id); + d.setMilliseconds(d.getMilliseconds() + ttl); + d.setMilliseconds(0); + return d; } export async function addCrawlJob(id: string, job_id: string) { - _logger.debug("Adding crawl job " + job_id + " to Redis...", { jobId: job_id, module: "crawl-redis", method: "addCrawlJob", crawlId: id }); - await redisConnection.sadd("crawl:" + id + ":jobs", job_id); - await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); + _logger.debug("Adding crawl job " + job_id + " to Redis...", { + jobId: job_id, + module: "crawl-redis", + method: "addCrawlJob", + crawlId: id + }); + await redisConnection.sadd("crawl:" + id + ":jobs", job_id); + await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); } export async function addCrawlJobs(id: string, job_ids: string[]) { - _logger.debug("Adding crawl jobs to Redis...", { jobIds: job_ids, module: "crawl-redis", method: "addCrawlJobs", crawlId: id }); - await redisConnection.sadd("crawl:" + id + ":jobs", ...job_ids); - await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); + _logger.debug("Adding crawl jobs to Redis...", { + jobIds: job_ids, + module: "crawl-redis", + method: "addCrawlJobs", + crawlId: id + }); + await redisConnection.sadd("crawl:" + id + ":jobs", ...job_ids); + await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); } -export async function addCrawlJobDone(id: string, job_id: string, success: boolean) { - _logger.debug("Adding done crawl job to Redis...", { jobId: job_id, module: "crawl-redis", method: "addCrawlJobDone", crawlId: id }); - await redisConnection.sadd("crawl:" + id + ":jobs_done", job_id); - await redisConnection.expire("crawl:" + id + ":jobs_done", 24 * 60 * 60, "NX"); +export async function addCrawlJobDone( + id: string, + job_id: string, + success: boolean +) { + _logger.debug("Adding done crawl job to Redis...", { + jobId: job_id, + module: "crawl-redis", + method: "addCrawlJobDone", + crawlId: id + }); + await redisConnection.sadd("crawl:" + id + ":jobs_done", job_id); + await redisConnection.expire( + "crawl:" + id + ":jobs_done", + 24 * 60 * 60, + "NX" + ); - if (success) { - await redisConnection.rpush("crawl:" + id + ":jobs_done_ordered", job_id); - await redisConnection.expire("crawl:" + id + ":jobs_done_ordered", 24 * 60 * 60, "NX"); - } + if (success) { + await redisConnection.rpush("crawl:" + id + ":jobs_done_ordered", job_id); + await redisConnection.expire( + "crawl:" + id + ":jobs_done_ordered", + 24 * 60 * 60, + "NX" + ); + } } export async function getDoneJobsOrderedLength(id: string): Promise { - return await redisConnection.llen("crawl:" + id + ":jobs_done_ordered"); + return await redisConnection.llen("crawl:" + id + ":jobs_done_ordered"); } -export async function getDoneJobsOrdered(id: string, start = 0, end = -1): Promise { - return await redisConnection.lrange("crawl:" + id + ":jobs_done_ordered", start, end); +export async function getDoneJobsOrdered( + id: string, + start = 0, + end = -1 +): Promise { + return await redisConnection.lrange( + "crawl:" + id + ":jobs_done_ordered", + start, + end + ); } export async function isCrawlFinished(id: string) { - return (await redisConnection.scard("crawl:" + id + ":jobs_done")) === (await redisConnection.scard("crawl:" + id + ":jobs")); + return ( + (await redisConnection.scard("crawl:" + id + ":jobs_done")) === + (await redisConnection.scard("crawl:" + id + ":jobs")) + ); } export async function isCrawlFinishedLocked(id: string) { - return (await redisConnection.exists("crawl:" + id + ":finish")); + return await redisConnection.exists("crawl:" + id + ":finish"); } export async function finishCrawl(id: string) { - if (await isCrawlFinished(id)) { - _logger.debug("Marking crawl as finished.", { module: "crawl-redis", method: "finishCrawl", crawlId: id }); - const set = await redisConnection.setnx("crawl:" + id + ":finish", "yes"); - if (set === 1) { - await redisConnection.expire("crawl:" + id + ":finish", 24 * 60 * 60); - } - return set === 1 - } else { - _logger.debug("Crawl can not be finished yet, not marking as finished.", { module: "crawl-redis", method: "finishCrawl", crawlId: id }); + if (await isCrawlFinished(id)) { + _logger.debug("Marking crawl as finished.", { + module: "crawl-redis", + method: "finishCrawl", + crawlId: id + }); + const set = await redisConnection.setnx("crawl:" + id + ":finish", "yes"); + if (set === 1) { + await redisConnection.expire("crawl:" + id + ":finish", 24 * 60 * 60); } + return set === 1; + } else { + _logger.debug("Crawl can not be finished yet, not marking as finished.", { + module: "crawl-redis", + method: "finishCrawl", + crawlId: id + }); + } } export async function getCrawlJobs(id: string): Promise { - return await redisConnection.smembers("crawl:" + id + ":jobs"); + return await redisConnection.smembers("crawl:" + id + ":jobs"); } export async function getThrottledJobs(teamId: string): Promise { - return await redisConnection.zrangebyscore("concurrency-limiter:" + teamId + ":throttled", Date.now(), Infinity); + return await redisConnection.zrangebyscore( + "concurrency-limiter:" + teamId + ":throttled", + Date.now(), + Infinity + ); } export function normalizeURL(url: string, sc: StoredCrawl): string { - const urlO = new URL(url); - if (!sc.crawlerOptions || sc.crawlerOptions.ignoreQueryParameters) { - urlO.search = ""; - } - urlO.hash = ""; - return urlO.href; + const urlO = new URL(url); + if (!sc.crawlerOptions || sc.crawlerOptions.ignoreQueryParameters) { + urlO.search = ""; + } + urlO.hash = ""; + return urlO.href; } export function generateURLPermutations(url: string | URL): URL[] { - const urlO = new URL(url); + const urlO = new URL(url); - // Construct two versions, one with www., one without - const urlWithWWW = new URL(urlO); - const urlWithoutWWW = new URL(urlO); - if (urlO.hostname.startsWith("www.")) { - urlWithoutWWW.hostname = urlWithWWW.hostname.slice(4); - } else { - urlWithWWW.hostname = "www." + urlWithoutWWW.hostname; + // Construct two versions, one with www., one without + const urlWithWWW = new URL(urlO); + const urlWithoutWWW = new URL(urlO); + if (urlO.hostname.startsWith("www.")) { + urlWithoutWWW.hostname = urlWithWWW.hostname.slice(4); + } else { + urlWithWWW.hostname = "www." + urlWithoutWWW.hostname; + } + + let permutations = [urlWithWWW, urlWithoutWWW]; + + // Construct more versions for http/https + permutations = permutations.flatMap((urlO) => { + if (!["http:", "https:"].includes(urlO.protocol)) { + return [urlO]; } - let permutations = [urlWithWWW, urlWithoutWWW]; + const urlWithHTTP = new URL(urlO); + const urlWithHTTPS = new URL(urlO); + urlWithHTTP.protocol = "http:"; + urlWithHTTPS.protocol = "https:"; - // Construct more versions for http/https - permutations = permutations.flatMap(urlO => { - if (!["http:", "https:"].includes(urlO.protocol)) { - return [urlO]; - } + return [urlWithHTTP, urlWithHTTPS]; + }); - const urlWithHTTP = new URL(urlO); - const urlWithHTTPS = new URL(urlO); - urlWithHTTP.protocol = "http:"; - urlWithHTTPS.protocol = "https:"; - - return [urlWithHTTP, urlWithHTTPS]; - }); - - return permutations; + return permutations; } -export async function lockURL(id: string, sc: StoredCrawl, url: string): Promise { - let logger = _logger.child({ crawlId: id, module: "crawl-redis", method: "lockURL", preNormalizedURL: url, teamId: sc.team_id, plan: sc.plan }); +export async function lockURL( + id: string, + sc: StoredCrawl, + url: string +): Promise { + let logger = _logger.child({ + crawlId: id, + module: "crawl-redis", + method: "lockURL", + preNormalizedURL: url, + teamId: sc.team_id, + plan: sc.plan + }); - if (typeof sc.crawlerOptions?.limit === "number") { - if (await redisConnection.scard("crawl:" + id + ":visited_unique") >= sc.crawlerOptions.limit) { - logger.debug("Crawl has already hit visited_unique limit, not locking URL."); - return false; - } + if (typeof sc.crawlerOptions?.limit === "number") { + if ( + (await redisConnection.scard("crawl:" + id + ":visited_unique")) >= + sc.crawlerOptions.limit + ) { + logger.debug( + "Crawl has already hit visited_unique limit, not locking URL." + ); + return false; } + } - url = normalizeURL(url, sc); - logger = logger.child({ url }); + url = normalizeURL(url, sc); + logger = logger.child({ url }); - await redisConnection.sadd("crawl:" + id + ":visited_unique", url); - await redisConnection.expire("crawl:" + id + ":visited_unique", 24 * 60 * 60, "NX"); + await redisConnection.sadd("crawl:" + id + ":visited_unique", url); + await redisConnection.expire( + "crawl:" + id + ":visited_unique", + 24 * 60 * 60, + "NX" + ); - let res: boolean; - if (!sc.crawlerOptions?.deduplicateSimilarURLs) { - res = (await redisConnection.sadd("crawl:" + id + ":visited", url)) !== 0 - } else { - const permutations = generateURLPermutations(url).map(x => x.href); - // logger.debug("Adding URL permutations for URL " + JSON.stringify(url) + "...", { permutations }); - const x = (await redisConnection.sadd("crawl:" + id + ":visited", ...permutations)); - res = x === permutations.length; - } + let res: boolean; + if (!sc.crawlerOptions?.deduplicateSimilarURLs) { + res = (await redisConnection.sadd("crawl:" + id + ":visited", url)) !== 0; + } else { + const permutations = generateURLPermutations(url).map((x) => x.href); + // logger.debug("Adding URL permutations for URL " + JSON.stringify(url) + "...", { permutations }); + const x = await redisConnection.sadd( + "crawl:" + id + ":visited", + ...permutations + ); + res = x === permutations.length; + } - await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); + await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); - logger.debug("Locking URL " + JSON.stringify(url) + "... result: " + res, { res }); - return res; + logger.debug("Locking URL " + JSON.stringify(url) + "... result: " + res, { + res + }); + return res; } /// NOTE: does not check limit. only use if limit is checked beforehand e.g. with sitemap -export async function lockURLs(id: string, sc: StoredCrawl, urls: string[]): Promise { - urls = urls.map(url => normalizeURL(url, sc)); - const logger = _logger.child({ crawlId: id, module: "crawl-redis", method: "lockURL", teamId: sc.team_id, plan: sc.plan }); +export async function lockURLs( + id: string, + sc: StoredCrawl, + urls: string[] +): Promise { + urls = urls.map((url) => normalizeURL(url, sc)); + const logger = _logger.child({ + crawlId: id, + module: "crawl-redis", + method: "lockURL", + teamId: sc.team_id, + plan: sc.plan + }); - // Add to visited_unique set - logger.debug("Locking " + urls.length + " URLs..."); - await redisConnection.sadd("crawl:" + id + ":visited_unique", ...urls); - await redisConnection.expire("crawl:" + id + ":visited_unique", 24 * 60 * 60, "NX"); + // Add to visited_unique set + logger.debug("Locking " + urls.length + " URLs..."); + await redisConnection.sadd("crawl:" + id + ":visited_unique", ...urls); + await redisConnection.expire( + "crawl:" + id + ":visited_unique", + 24 * 60 * 60, + "NX" + ); - let res: boolean; - if (!sc.crawlerOptions?.deduplicateSimilarURLs) { - const x = await redisConnection.sadd("crawl:" + id + ":visited", ...urls); - res = x === urls.length; - } else { - const allPermutations = urls.flatMap(url => generateURLPermutations(url).map(x => x.href)); - logger.debug("Adding " + allPermutations.length + " URL permutations..."); - const x = await redisConnection.sadd("crawl:" + id + ":visited", ...allPermutations); - res = x === allPermutations.length; - } + let res: boolean; + if (!sc.crawlerOptions?.deduplicateSimilarURLs) { + const x = await redisConnection.sadd("crawl:" + id + ":visited", ...urls); + res = x === urls.length; + } else { + const allPermutations = urls.flatMap((url) => + generateURLPermutations(url).map((x) => x.href) + ); + logger.debug("Adding " + allPermutations.length + " URL permutations..."); + const x = await redisConnection.sadd( + "crawl:" + id + ":visited", + ...allPermutations + ); + res = x === allPermutations.length; + } - await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); + await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); - logger.debug("lockURLs final result: " + res, { res }); - return res; + logger.debug("lockURLs final result: " + res, { res }); + return res; } -export function crawlToCrawler(id: string, sc: StoredCrawl, newBase?: string): WebCrawler { - const crawler = new WebCrawler({ - jobId: id, - initialUrl: sc.originUrl!, - baseUrl: newBase ? new URL(newBase).origin : undefined, - includes: sc.crawlerOptions?.includes ?? [], - excludes: sc.crawlerOptions?.excludes ?? [], - maxCrawledLinks: sc.crawlerOptions?.maxCrawledLinks ?? 1000, - maxCrawledDepth: getAdjustedMaxDepth(sc.originUrl!, sc.crawlerOptions?.maxDepth ?? 10), - limit: sc.crawlerOptions?.limit ?? 10000, - generateImgAltText: sc.crawlerOptions?.generateImgAltText ?? false, - allowBackwardCrawling: sc.crawlerOptions?.allowBackwardCrawling ?? false, - allowExternalContentLinks: sc.crawlerOptions?.allowExternalContentLinks ?? false, - allowSubdomains: sc.crawlerOptions?.allowSubdomains ?? false, - ignoreRobotsTxt: sc.crawlerOptions?.ignoreRobotsTxt ?? false, - }); +export function crawlToCrawler( + id: string, + sc: StoredCrawl, + newBase?: string +): WebCrawler { + const crawler = new WebCrawler({ + jobId: id, + initialUrl: sc.originUrl!, + baseUrl: newBase ? new URL(newBase).origin : undefined, + includes: sc.crawlerOptions?.includes ?? [], + excludes: sc.crawlerOptions?.excludes ?? [], + maxCrawledLinks: sc.crawlerOptions?.maxCrawledLinks ?? 1000, + maxCrawledDepth: getAdjustedMaxDepth( + sc.originUrl!, + sc.crawlerOptions?.maxDepth ?? 10 + ), + limit: sc.crawlerOptions?.limit ?? 10000, + generateImgAltText: sc.crawlerOptions?.generateImgAltText ?? false, + allowBackwardCrawling: sc.crawlerOptions?.allowBackwardCrawling ?? false, + allowExternalContentLinks: + sc.crawlerOptions?.allowExternalContentLinks ?? false, + allowSubdomains: sc.crawlerOptions?.allowSubdomains ?? false, + ignoreRobotsTxt: sc.crawlerOptions?.ignoreRobotsTxt ?? false + }); - if (sc.robots !== undefined) { - try { - crawler.importRobotsTxt(sc.robots); - } catch (_) {} - } + if (sc.robots !== undefined) { + try { + crawler.importRobotsTxt(sc.robots); + } catch (_) {} + } - return crawler; + return crawler; } diff --git a/apps/api/src/lib/custom-error.ts b/apps/api/src/lib/custom-error.ts index 2ffe52e9..25502a8e 100644 --- a/apps/api/src/lib/custom-error.ts +++ b/apps/api/src/lib/custom-error.ts @@ -8,7 +8,7 @@ export class CustomError extends Error { statusCode: number, status: string, message: string = "", - dataIngestionJob?: any, + dataIngestionJob?: any ) { super(message); this.statusCode = statusCode; @@ -19,4 +19,3 @@ export class CustomError extends Error { Object.setPrototypeOf(this, CustomError.prototype); } } - diff --git a/apps/api/src/lib/default-values.ts b/apps/api/src/lib/default-values.ts index f70f17c0..ceca176c 100644 --- a/apps/api/src/lib/default-values.ts +++ b/apps/api/src/lib/default-values.ts @@ -14,15 +14,15 @@ export const defaultPageOptions = { export const defaultCrawlerOptions = { allowBackwardCrawling: false, limit: 10000 -} +}; export const defaultCrawlPageOptions = { onlyMainContent: false, includeHtml: false, removeTags: [], parsePDF: true -} +}; export const defaultExtractorOptions = { mode: "markdown" -} \ No newline at end of file +}; diff --git a/apps/api/src/lib/entities.ts b/apps/api/src/lib/entities.ts index 9fa39cff..93911485 100644 --- a/apps/api/src/lib/entities.ts +++ b/apps/api/src/lib/entities.ts @@ -12,32 +12,40 @@ export interface Progress { currentDocument?: Document; } -export type Action = { - type: "wait", - milliseconds?: number, - selector?: string, -} | { - type: "click", - selector: string, -} | { - type: "screenshot", - fullPage?: boolean, -} | { - type: "write", - text: string, -} | { - type: "press", - key: string, -} | { - type: "scroll", - direction?: "up" | "down", - selector?: string, -} | { - type: "scrape", -} | { - type: "executeJavascript", - script: string, -} +export type Action = + | { + type: "wait"; + milliseconds?: number; + selector?: string; + } + | { + type: "click"; + selector: string; + } + | { + type: "screenshot"; + fullPage?: boolean; + } + | { + type: "write"; + text: string; + } + | { + type: "press"; + key: string; + } + | { + type: "scroll"; + direction?: "up" | "down"; + selector?: string; + } + | { + type: "scrape"; + } + | { + type: "executeJavascript"; + script: string; + }; export type PageOptions = { includeMarkdown?: boolean; @@ -69,11 +77,15 @@ export type PageOptions = { }; export type ExtractorOptions = { - mode: "markdown" | "llm-extraction" | "llm-extraction-from-markdown" | "llm-extraction-from-raw-html"; + mode: + | "markdown" + | "llm-extraction" + | "llm-extraction-from-markdown" + | "llm-extraction-from-raw-html"; extractionPrompt?: string; extractionSchema?: Record; userPrompt?: string; -} +}; export type SearchOptions = { limit?: number; @@ -97,7 +109,7 @@ export type CrawlerOptions = { mode?: "default" | "fast"; // have a mode of some sort allowBackwardCrawling?: boolean; allowExternalContentLinks?: boolean; -} +}; export type WebScraperOptions = { jobId: string; @@ -137,11 +149,11 @@ export class Document { actions?: { screenshots?: string[]; scrapes?: ScrapeActionContent[]; - } + }; index?: number; linksOnPage?: string[]; // Add this new field as a separate property - + constructor(data: Partial) { if (!data.content) { throw new Error("Missing required fields"); @@ -158,20 +170,19 @@ export class Document { } } - export class SearchResult { url: string; title: string; description: string; constructor(url: string, title: string, description: string) { - this.url = url; - this.title = title; - this.description = description; + this.url = url; + this.title = title; + this.description = description; } toString(): string { - return `SearchResult(url=${this.url}, title=${this.title}, description=${this.description})`; + return `SearchResult(url=${this.url}, title=${this.title}, description=${this.description})`; } } @@ -188,8 +199,7 @@ export interface FireEngineResponse { scrapeActionContent?: ScrapeActionContent[]; } - -export interface FireEngineOptions{ +export interface FireEngineOptions { mobileProxy?: boolean; method?: string; engine?: string; diff --git a/apps/api/src/lib/extract/build-document.ts b/apps/api/src/lib/extract/build-document.ts index 66417a07..79453313 100644 --- a/apps/api/src/lib/extract/build-document.ts +++ b/apps/api/src/lib/extract/build-document.ts @@ -5,9 +5,11 @@ export function buildDocument(document: Document): string { const markdown = document.markdown; // for each key in the metadata allow up to 250 characters - const metadataString = Object.entries(metadata).map(([key, value]) => { - return `${key}: ${value?.toString().slice(0, 250)}`; - }).join('\n'); + const metadataString = Object.entries(metadata) + .map(([key, value]) => { + return `${key}: ${value?.toString().slice(0, 250)}`; + }) + .join("\n"); const documentMetadataString = `\n- - - - - Page metadata - - - - -\n${metadataString}`; const documentString = `${markdown}${documentMetadataString}`; diff --git a/apps/api/src/lib/extract/reranker.ts b/apps/api/src/lib/extract/reranker.ts index 30aca441..044f71a4 100644 --- a/apps/api/src/lib/extract/reranker.ts +++ b/apps/api/src/lib/extract/reranker.ts @@ -1,7 +1,7 @@ import { CohereClient } from "cohere-ai"; import { MapDocument } from "../../controllers/v1/types"; const cohere = new CohereClient({ - token: process.env.COHERE_API_KEY, + token: process.env.COHERE_API_KEY }); export async function rerankDocuments( @@ -15,8 +15,14 @@ export async function rerankDocuments( query, topN, model, - returnDocuments: true, + returnDocuments: true }); - return rerank.results.sort((a, b) => b.relevanceScore - a.relevanceScore).map(x => ({ document: x.document, index: x.index, relevanceScore: x.relevanceScore })); + return rerank.results + .sort((a, b) => b.relevanceScore - a.relevanceScore) + .map((x) => ({ + document: x.document, + index: x.index, + relevanceScore: x.relevanceScore + })); } diff --git a/apps/api/src/lib/html-to-markdown.ts b/apps/api/src/lib/html-to-markdown.ts index 92bcd4cd..7a0020d1 100644 --- a/apps/api/src/lib/html-to-markdown.ts +++ b/apps/api/src/lib/html-to-markdown.ts @@ -1,16 +1,20 @@ - -import koffi from 'koffi'; -import { join } from 'path'; -import "../services/sentry" +import koffi from "koffi"; +import { join } from "path"; +import "../services/sentry"; import * as Sentry from "@sentry/node"; -import dotenv from 'dotenv'; -import { logger } from './logger'; -import { stat } from 'fs/promises'; +import dotenv from "dotenv"; +import { logger } from "./logger"; +import { stat } from "fs/promises"; dotenv.config(); // TODO: add a timeout to the Go parser -const goExecutablePath = join(process.cwd(), 'sharedLibs', 'go-html-to-md', 'html-to-markdown.so'); +const goExecutablePath = join( + process.cwd(), + "sharedLibs", + "go-html-to-md", + "html-to-markdown.so" +); class GoMarkdownConverter { private static instance: GoMarkdownConverter; @@ -18,7 +22,7 @@ class GoMarkdownConverter { private constructor() { const lib = koffi.load(goExecutablePath); - this.convert = lib.func('ConvertHTMLToMarkdown', 'string', ['string']); + this.convert = lib.func("ConvertHTMLToMarkdown", "string", ["string"]); } public static async getInstance(): Promise { @@ -46,9 +50,11 @@ class GoMarkdownConverter { } } -export async function parseMarkdown(html: string | null | undefined): Promise { +export async function parseMarkdown( + html: string | null | undefined +): Promise { if (!html) { - return ''; + return ""; } try { @@ -62,17 +68,25 @@ export async function parseMarkdown(html: string | null | undefined): Promise - `${info.timestamp} ${info.level} [${info.metadata.module ?? ""}:${info.metadata.method ?? ""}]: ${info.message} ${info.level.includes("error") || info.level.includes("warn") ? JSON.stringify( - info.metadata, - (_, value) => { - if (value instanceof Error) { - return { - ...value, - name: value.name, - message: value.message, - stack: value.stack, - cause: value.cause, - } - } else { - return value; - } - } - ) : ""}` -) +const logFormat = winston.format.printf( + (info) => + `${info.timestamp} ${info.level} [${info.metadata.module ?? ""}:${info.metadata.method ?? ""}]: ${info.message} ${ + info.level.includes("error") || info.level.includes("warn") + ? JSON.stringify(info.metadata, (_, value) => { + if (value instanceof Error) { + return { + ...value, + name: value.name, + message: value.message, + stack: value.stack, + cause: value.cause + }; + } else { + return value; + } + }) + : "" + }` +); export const logger = winston.createLogger({ level: process.env.LOGGING_LEVEL?.toLowerCase() ?? "debug", @@ -32,8 +34,8 @@ export const logger = winston.createLogger({ name: value.name, message: value.message, stack: value.stack, - cause: value.cause, - } + cause: value.cause + }; } else { return value; } @@ -43,9 +45,15 @@ export const logger = winston.createLogger({ new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - winston.format.metadata({ fillExcept: ["message", "level", "timestamp"] }), - ...(((process.env.ENV === "production" && process.env.SENTRY_ENVIRONMENT === "dev") || (process.env.ENV !== "production")) ? [winston.format.colorize(), logFormat] : []), - ), - }), - ], + winston.format.metadata({ + fillExcept: ["message", "level", "timestamp"] + }), + ...((process.env.ENV === "production" && + process.env.SENTRY_ENVIRONMENT === "dev") || + process.env.ENV !== "production" + ? [winston.format.colorize(), logFormat] + : []) + ) + }) + ] }); diff --git a/apps/api/src/lib/parseApi.ts b/apps/api/src/lib/parseApi.ts index 4b03a405..e29135fd 100644 --- a/apps/api/src/lib/parseApi.ts +++ b/apps/api/src/lib/parseApi.ts @@ -13,7 +13,6 @@ export function parseApi(api: string) { return uuid; } - export function uuidToFcUuid(uuid: string) { const uuidWithoutDashes = uuid.replace(/-/g, ""); return `fc-${uuidWithoutDashes}`; diff --git a/apps/api/src/lib/ranker.test.ts b/apps/api/src/lib/ranker.test.ts index 6d17a08b..2b30de19 100644 --- a/apps/api/src/lib/ranker.test.ts +++ b/apps/api/src/lib/ranker.test.ts @@ -1,64 +1,61 @@ -import { performRanking } from './ranker'; +import { performRanking } from "./ranker"; -describe('performRanking', () => { - it('should rank links based on similarity to search query', async () => { +describe("performRanking", () => { + it("should rank links based on similarity to search query", async () => { const linksWithContext = [ - 'url: https://example.com/dogs, title: All about dogs, description: Learn about different dog breeds', - 'url: https://example.com/cats, title: Cat care guide, description: Everything about cats', - 'url: https://example.com/pets, title: General pet care, description: Care for all types of pets' + "url: https://example.com/dogs, title: All about dogs, description: Learn about different dog breeds", + "url: https://example.com/cats, title: Cat care guide, description: Everything about cats", + "url: https://example.com/pets, title: General pet care, description: Care for all types of pets" ]; const links = [ - 'https://example.com/dogs', - 'https://example.com/cats', - 'https://example.com/pets' + "https://example.com/dogs", + "https://example.com/cats", + "https://example.com/pets" ]; - const searchQuery = 'cats training'; + const searchQuery = "cats training"; const result = await performRanking(linksWithContext, links, searchQuery); // Should return array of objects with link, linkWithContext, score, originalIndex expect(result).toBeInstanceOf(Array); expect(result.length).toBe(3); - + // First result should be the dogs page since query is about dogs - expect(result[0].link).toBe('https://example.com/cats'); - + expect(result[0].link).toBe("https://example.com/cats"); + // Each result should have required properties - result.forEach(item => { - expect(item).toHaveProperty('link'); - expect(item).toHaveProperty('linkWithContext'); - expect(item).toHaveProperty('score'); - expect(item).toHaveProperty('originalIndex'); - expect(typeof item.score).toBe('number'); + result.forEach((item) => { + expect(item).toHaveProperty("link"); + expect(item).toHaveProperty("linkWithContext"); + expect(item).toHaveProperty("score"); + expect(item).toHaveProperty("originalIndex"); + expect(typeof item.score).toBe("number"); expect(item.score).toBeGreaterThanOrEqual(0); expect(item.score).toBeLessThanOrEqual(1); }); // Scores should be in descending order for (let i = 1; i < result.length; i++) { - expect(result[i].score).toBeLessThanOrEqual(result[i-1].score); + expect(result[i].score).toBeLessThanOrEqual(result[i - 1].score); } }); - it('should handle empty inputs', async () => { - const result = await performRanking([], [], ''); + it("should handle empty inputs", async () => { + const result = await performRanking([], [], ""); expect(result).toEqual([]); }); - it('should maintain original order for equal scores', async () => { + it("should maintain original order for equal scores", async () => { const linksWithContext = [ - 'url: https://example.com/1, title: Similar content A, description: test', - 'url: https://example.com/2, title: Similar content B, description: test' + "url: https://example.com/1, title: Similar content A, description: test", + "url: https://example.com/2, title: Similar content B, description: test" ]; - const links = [ - 'https://example.com/1', - 'https://example.com/2' - ]; + const links = ["https://example.com/1", "https://example.com/2"]; - const searchQuery = 'test'; + const searchQuery = "test"; const result = await performRanking(linksWithContext, links, searchQuery); diff --git a/apps/api/src/lib/ranker.ts b/apps/api/src/lib/ranker.ts index e7fa235c..2f06d76d 100644 --- a/apps/api/src/lib/ranker.ts +++ b/apps/api/src/lib/ranker.ts @@ -1,18 +1,18 @@ -import axios from 'axios'; -import { configDotenv } from 'dotenv'; +import axios from "axios"; +import { configDotenv } from "dotenv"; import OpenAI from "openai"; configDotenv(); const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.OPENAI_API_KEY }); async function getEmbedding(text: string) { const embedding = await openai.embeddings.create({ model: "text-embedding-ada-002", input: text, - encoding_format: "float", + encoding_format: "float" }); return embedding.data[0].embedding; @@ -20,12 +20,8 @@ async function getEmbedding(text: string) { const cosineSimilarity = (vec1: number[], vec2: number[]): number => { const dotProduct = vec1.reduce((sum, val, i) => sum + val * vec2[i], 0); - const magnitude1 = Math.sqrt( - vec1.reduce((sum, val) => sum + val * val, 0) - ); - const magnitude2 = Math.sqrt( - vec2.reduce((sum, val) => sum + val * val, 0) - ); + const magnitude1 = Math.sqrt(vec1.reduce((sum, val) => sum + val * val, 0)); + const magnitude2 = Math.sqrt(vec2.reduce((sum, val) => sum + val * val, 0)); if (magnitude1 === 0 || magnitude2 === 0) return 0; return dotProduct / (magnitude1 * magnitude2); }; @@ -40,7 +36,11 @@ const textToVector = (searchQuery: string, text: string): number[] => { }); }; -async function performRanking(linksWithContext: string[], links: string[], searchQuery: string) { +async function performRanking( + linksWithContext: string[], + links: string[], + searchQuery: string +) { try { // Handle invalid inputs if (!searchQuery || !linksWithContext.length || !links.length) { @@ -54,27 +54,29 @@ async function performRanking(linksWithContext: string[], links: string[], searc const queryEmbedding = await getEmbedding(sanitizedQuery); // Generate embeddings for each link and calculate similarity - const linksAndScores = await Promise.all(linksWithContext.map(async (linkWithContext, index) => { - try { - const linkEmbedding = await getEmbedding(linkWithContext); - const score = cosineSimilarity(queryEmbedding, linkEmbedding); - - return { - link: links[index], - linkWithContext, - score, - originalIndex: index - }; - } catch (err) { - // If embedding fails for a link, return with score 0 - return { - link: links[index], - linkWithContext, - score: 0, - originalIndex: index - }; - } - })); + const linksAndScores = await Promise.all( + linksWithContext.map(async (linkWithContext, index) => { + try { + const linkEmbedding = await getEmbedding(linkWithContext); + const score = cosineSimilarity(queryEmbedding, linkEmbedding); + + return { + link: links[index], + linkWithContext, + score, + originalIndex: index + }; + } catch (err) { + // If embedding fails for a link, return with score 0 + return { + link: links[index], + linkWithContext, + score: 0, + originalIndex: index + }; + } + }) + ); // Sort links based on similarity scores while preserving original order for equal scores linksAndScores.sort((a, b) => { diff --git a/apps/api/src/lib/scrape-events.ts b/apps/api/src/lib/scrape-events.ts index 83873a58..6c39c722 100644 --- a/apps/api/src/lib/scrape-events.ts +++ b/apps/api/src/lib/scrape-events.ts @@ -6,47 +6,61 @@ import { Engine } from "../scraper/scrapeURL/engines"; configDotenv(); export type ScrapeErrorEvent = { - type: "error", - message: string, - stack?: string, -} + type: "error"; + message: string; + stack?: string; +}; export type ScrapeScrapeEvent = { - type: "scrape", - url: string, - worker?: string, - method: Engine, + type: "scrape"; + url: string; + worker?: string; + method: Engine; result: null | { - success: boolean, - response_code?: number, - response_size?: number, - error?: string | object, + success: boolean; + response_code?: number; + response_size?: number; + error?: string | object; // proxy?: string, - time_taken: number, - }, -} + time_taken: number; + }; +}; export type ScrapeQueueEvent = { - type: "queue", - event: "waiting" | "active" | "completed" | "paused" | "resumed" | "removed" | "failed", - worker?: string, -} + type: "queue"; + event: + | "waiting" + | "active" + | "completed" + | "paused" + | "resumed" + | "removed" + | "failed"; + worker?: string; +}; -export type ScrapeEvent = ScrapeErrorEvent | ScrapeScrapeEvent | ScrapeQueueEvent; +export type ScrapeEvent = + | ScrapeErrorEvent + | ScrapeScrapeEvent + | ScrapeQueueEvent; export class ScrapeEvents { static async insert(jobId: string, content: ScrapeEvent) { if (jobId === "TEST") return null; - - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (useDbAuthentication) { try { - const result = await supabase.from("scrape_events").insert({ - job_id: jobId, - type: content.type, - content: content, - // created_at - }).select().single(); + const result = await supabase + .from("scrape_events") + .insert({ + job_id: jobId, + type: content.type, + content: content + // created_at + }) + .select() + .single(); return (result.data as any).id; } catch (error) { // logger.error(`Error inserting scrape event: ${error}`); @@ -57,17 +71,25 @@ export class ScrapeEvents { return null; } - static async updateScrapeResult(logId: number | null, result: ScrapeScrapeEvent["result"]) { + static async updateScrapeResult( + logId: number | null, + result: ScrapeScrapeEvent["result"] + ) { if (logId === null) return; try { - const previousLog = (await supabase.from("scrape_events").select().eq("id", logId).single()).data as any; - await supabase.from("scrape_events").update({ - content: { - ...previousLog.content, - result, - } - }).eq("id", logId); + const previousLog = ( + await supabase.from("scrape_events").select().eq("id", logId).single() + ).data as any; + await supabase + .from("scrape_events") + .update({ + content: { + ...previousLog.content, + result + } + }) + .eq("id", logId); } catch (error) { logger.error(`Error updating scrape result: ${error}`); } @@ -78,7 +100,7 @@ export class ScrapeEvents { await this.insert(((job as any).id ? (job as any).id : job) as string, { type: "queue", event, - worker: process.env.FLY_MACHINE_ID, + worker: process.env.FLY_MACHINE_ID }); } catch (error) { logger.error(`Error logging job event: ${error}`); diff --git a/apps/api/src/lib/supabase-jobs.ts b/apps/api/src/lib/supabase-jobs.ts index c9be72a3..2ed7c02a 100644 --- a/apps/api/src/lib/supabase-jobs.ts +++ b/apps/api/src/lib/supabase-jobs.ts @@ -58,7 +58,7 @@ export const supabaseGetJobsByCrawlId = async (crawlId: string) => { const { data, error } = await supabase_service .from("firecrawl_jobs") .select() - .eq("crawl_id", crawlId) + .eq("crawl_id", crawlId); if (error) { logger.error(`Error in supabaseGetJobsByCrawlId: ${error}`); @@ -73,7 +73,6 @@ export const supabaseGetJobsByCrawlId = async (crawlId: string) => { return data; }; - export const supabaseGetJobByIdOnlyData = async (jobId: string) => { const { data, error } = await supabase_service .from("firecrawl_jobs") @@ -90,4 +89,4 @@ export const supabaseGetJobByIdOnlyData = async (jobId: string) => { } return data; -}; \ No newline at end of file +}; diff --git a/apps/api/src/lib/timeout.ts b/apps/api/src/lib/timeout.ts index 46d34a5a..f913817a 100644 --- a/apps/api/src/lib/timeout.ts +++ b/apps/api/src/lib/timeout.ts @@ -1 +1 @@ -export const axiosTimeout = 5000; \ No newline at end of file +export const axiosTimeout = 5000; diff --git a/apps/api/src/lib/validate-country.ts b/apps/api/src/lib/validate-country.ts index bff1c25c..797ea542 100644 --- a/apps/api/src/lib/validate-country.ts +++ b/apps/api/src/lib/validate-country.ts @@ -6,7 +6,7 @@ export const countries = { continent: "EU", capital: "Andorra la Vella", currency: ["EUR"], - languages: ["ca"], + languages: ["ca"] }, AE: { name: "United Arab Emirates", @@ -15,7 +15,7 @@ export const countries = { continent: "AS", capital: "Abu Dhabi", currency: ["AED"], - languages: ["ar"], + languages: ["ar"] }, AF: { name: "Afghanistan", @@ -24,7 +24,7 @@ export const countries = { continent: "AS", capital: "Kabul", currency: ["AFN"], - languages: ["ps", "uz", "tk"], + languages: ["ps", "uz", "tk"] }, AG: { name: "Antigua and Barbuda", @@ -33,7 +33,7 @@ export const countries = { continent: "NA", capital: "Saint John's", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, AI: { name: "Anguilla", @@ -42,7 +42,7 @@ export const countries = { continent: "NA", capital: "The Valley", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, AL: { name: "Albania", @@ -51,7 +51,7 @@ export const countries = { continent: "EU", capital: "Tirana", currency: ["ALL"], - languages: ["sq"], + languages: ["sq"] }, AM: { name: "Armenia", @@ -60,7 +60,7 @@ export const countries = { continent: "AS", capital: "Yerevan", currency: ["AMD"], - languages: ["hy", "ru"], + languages: ["hy", "ru"] }, AO: { name: "Angola", @@ -69,7 +69,7 @@ export const countries = { continent: "AF", capital: "Luanda", currency: ["AOA"], - languages: ["pt"], + languages: ["pt"] }, AQ: { name: "Antarctica", @@ -78,7 +78,7 @@ export const countries = { continent: "AN", capital: "", currency: [], - languages: [], + languages: [] }, AR: { name: "Argentina", @@ -87,7 +87,7 @@ export const countries = { continent: "SA", capital: "Buenos Aires", currency: ["ARS"], - languages: ["es", "gn"], + languages: ["es", "gn"] }, AS: { name: "American Samoa", @@ -96,7 +96,7 @@ export const countries = { continent: "OC", capital: "Pago Pago", currency: ["USD"], - languages: ["en", "sm"], + languages: ["en", "sm"] }, AT: { name: "Austria", @@ -105,7 +105,7 @@ export const countries = { continent: "EU", capital: "Vienna", currency: ["EUR"], - languages: ["de"], + languages: ["de"] }, AU: { name: "Australia", @@ -114,7 +114,7 @@ export const countries = { continent: "OC", capital: "Canberra", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, AW: { name: "Aruba", @@ -123,7 +123,7 @@ export const countries = { continent: "NA", capital: "Oranjestad", currency: ["AWG"], - languages: ["nl", "pa"], + languages: ["nl", "pa"] }, AX: { name: "Aland", @@ -133,7 +133,7 @@ export const countries = { capital: "Mariehamn", currency: ["EUR"], languages: ["sv"], - partOf: "FI", + partOf: "FI" }, AZ: { name: "Azerbaijan", @@ -143,7 +143,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Baku", currency: ["AZN"], - languages: ["az"], + languages: ["az"] }, BA: { name: "Bosnia and Herzegovina", @@ -152,7 +152,7 @@ export const countries = { continent: "EU", capital: "Sarajevo", currency: ["BAM"], - languages: ["bs", "hr", "sr"], + languages: ["bs", "hr", "sr"] }, BB: { name: "Barbados", @@ -161,7 +161,7 @@ export const countries = { continent: "NA", capital: "Bridgetown", currency: ["BBD"], - languages: ["en"], + languages: ["en"] }, BD: { name: "Bangladesh", @@ -170,7 +170,7 @@ export const countries = { continent: "AS", capital: "Dhaka", currency: ["BDT"], - languages: ["bn"], + languages: ["bn"] }, BE: { name: "Belgium", @@ -179,7 +179,7 @@ export const countries = { continent: "EU", capital: "Brussels", currency: ["EUR"], - languages: ["nl", "fr", "de"], + languages: ["nl", "fr", "de"] }, BF: { name: "Burkina Faso", @@ -188,7 +188,7 @@ export const countries = { continent: "AF", capital: "Ouagadougou", currency: ["XOF"], - languages: ["fr", "ff"], + languages: ["fr", "ff"] }, BG: { name: "Bulgaria", @@ -197,7 +197,7 @@ export const countries = { continent: "EU", capital: "Sofia", currency: ["BGN"], - languages: ["bg"], + languages: ["bg"] }, BH: { name: "Bahrain", @@ -206,7 +206,7 @@ export const countries = { continent: "AS", capital: "Manama", currency: ["BHD"], - languages: ["ar"], + languages: ["ar"] }, BI: { name: "Burundi", @@ -215,7 +215,7 @@ export const countries = { continent: "AF", capital: "Bujumbura", currency: ["BIF"], - languages: ["fr", "rn"], + languages: ["fr", "rn"] }, BJ: { name: "Benin", @@ -224,7 +224,7 @@ export const countries = { continent: "AF", capital: "Porto-Novo", currency: ["XOF"], - languages: ["fr"], + languages: ["fr"] }, BL: { name: "Saint Barthelemy", @@ -233,7 +233,7 @@ export const countries = { continent: "NA", capital: "Gustavia", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, BM: { name: "Bermuda", @@ -242,7 +242,7 @@ export const countries = { continent: "NA", capital: "Hamilton", currency: ["BMD"], - languages: ["en"], + languages: ["en"] }, BN: { name: "Brunei", @@ -251,7 +251,7 @@ export const countries = { continent: "AS", capital: "Bandar Seri Begawan", currency: ["BND"], - languages: ["ms"], + languages: ["ms"] }, BO: { name: "Bolivia", @@ -260,7 +260,7 @@ export const countries = { continent: "SA", capital: "Sucre", currency: ["BOB", "BOV"], - languages: ["es", "ay", "qu"], + languages: ["es", "ay", "qu"] }, BQ: { name: "Bonaire", @@ -269,7 +269,7 @@ export const countries = { continent: "NA", capital: "Kralendijk", currency: ["USD"], - languages: ["nl"], + languages: ["nl"] }, BR: { name: "Brazil", @@ -278,7 +278,7 @@ export const countries = { continent: "SA", capital: "Brasília", currency: ["BRL"], - languages: ["pt"], + languages: ["pt"] }, BS: { name: "Bahamas", @@ -287,7 +287,7 @@ export const countries = { continent: "NA", capital: "Nassau", currency: ["BSD"], - languages: ["en"], + languages: ["en"] }, BT: { name: "Bhutan", @@ -296,7 +296,7 @@ export const countries = { continent: "AS", capital: "Thimphu", currency: ["BTN", "INR"], - languages: ["dz"], + languages: ["dz"] }, BV: { name: "Bouvet Island", @@ -305,7 +305,7 @@ export const countries = { continent: "AN", capital: "", currency: ["NOK"], - languages: ["no", "nb", "nn"], + languages: ["no", "nb", "nn"] }, BW: { name: "Botswana", @@ -314,7 +314,7 @@ export const countries = { continent: "AF", capital: "Gaborone", currency: ["BWP"], - languages: ["en", "tn"], + languages: ["en", "tn"] }, BY: { name: "Belarus", @@ -323,7 +323,7 @@ export const countries = { continent: "EU", capital: "Minsk", currency: ["BYN"], - languages: ["be", "ru"], + languages: ["be", "ru"] }, BZ: { name: "Belize", @@ -332,7 +332,7 @@ export const countries = { continent: "NA", capital: "Belmopan", currency: ["BZD"], - languages: ["en", "es"], + languages: ["en", "es"] }, CA: { name: "Canada", @@ -341,7 +341,7 @@ export const countries = { continent: "NA", capital: "Ottawa", currency: ["CAD"], - languages: ["en", "fr"], + languages: ["en", "fr"] }, CC: { name: "Cocos (Keeling) Islands", @@ -350,7 +350,7 @@ export const countries = { continent: "AS", capital: "West Island", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, CD: { name: "Democratic Republic of the Congo", @@ -359,7 +359,7 @@ export const countries = { continent: "AF", capital: "Kinshasa", currency: ["CDF"], - languages: ["fr", "ln", "kg", "sw", "lu"], + languages: ["fr", "ln", "kg", "sw", "lu"] }, CF: { name: "Central African Republic", @@ -368,7 +368,7 @@ export const countries = { continent: "AF", capital: "Bangui", currency: ["XAF"], - languages: ["fr", "sg"], + languages: ["fr", "sg"] }, CG: { name: "Republic of the Congo", @@ -377,7 +377,7 @@ export const countries = { continent: "AF", capital: "Brazzaville", currency: ["XAF"], - languages: ["fr", "ln"], + languages: ["fr", "ln"] }, CH: { name: "Switzerland", @@ -386,7 +386,7 @@ export const countries = { continent: "EU", capital: "Bern", currency: ["CHE", "CHF", "CHW"], - languages: ["de", "fr", "it"], + languages: ["de", "fr", "it"] }, CI: { name: "Ivory Coast", @@ -395,7 +395,7 @@ export const countries = { continent: "AF", capital: "Yamoussoukro", currency: ["XOF"], - languages: ["fr"], + languages: ["fr"] }, CK: { name: "Cook Islands", @@ -404,7 +404,7 @@ export const countries = { continent: "OC", capital: "Avarua", currency: ["NZD"], - languages: ["en"], + languages: ["en"] }, CL: { name: "Chile", @@ -413,7 +413,7 @@ export const countries = { continent: "SA", capital: "Santiago", currency: ["CLF", "CLP"], - languages: ["es"], + languages: ["es"] }, CM: { name: "Cameroon", @@ -422,7 +422,7 @@ export const countries = { continent: "AF", capital: "Yaoundé", currency: ["XAF"], - languages: ["en", "fr"], + languages: ["en", "fr"] }, CN: { name: "China", @@ -431,7 +431,7 @@ export const countries = { continent: "AS", capital: "Beijing", currency: ["CNY"], - languages: ["zh"], + languages: ["zh"] }, CO: { name: "Colombia", @@ -440,7 +440,7 @@ export const countries = { continent: "SA", capital: "Bogotá", currency: ["COP"], - languages: ["es"], + languages: ["es"] }, CR: { name: "Costa Rica", @@ -449,7 +449,7 @@ export const countries = { continent: "NA", capital: "San José", currency: ["CRC"], - languages: ["es"], + languages: ["es"] }, CU: { name: "Cuba", @@ -458,7 +458,7 @@ export const countries = { continent: "NA", capital: "Havana", currency: ["CUC", "CUP"], - languages: ["es"], + languages: ["es"] }, CV: { name: "Cape Verde", @@ -467,7 +467,7 @@ export const countries = { continent: "AF", capital: "Praia", currency: ["CVE"], - languages: ["pt"], + languages: ["pt"] }, CW: { name: "Curacao", @@ -476,7 +476,7 @@ export const countries = { continent: "NA", capital: "Willemstad", currency: ["ANG"], - languages: ["nl", "pa", "en"], + languages: ["nl", "pa", "en"] }, CX: { name: "Christmas Island", @@ -485,7 +485,7 @@ export const countries = { continent: "AS", capital: "Flying Fish Cove", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, CY: { name: "Cyprus", @@ -494,7 +494,7 @@ export const countries = { continent: "EU", capital: "Nicosia", currency: ["EUR"], - languages: ["el", "tr", "hy"], + languages: ["el", "tr", "hy"] }, CZ: { name: "Czech Republic", @@ -503,7 +503,7 @@ export const countries = { continent: "EU", capital: "Prague", currency: ["CZK"], - languages: ["cs"], + languages: ["cs"] }, DE: { name: "Germany", @@ -512,7 +512,7 @@ export const countries = { continent: "EU", capital: "Berlin", currency: ["EUR"], - languages: ["de"], + languages: ["de"] }, DJ: { name: "Djibouti", @@ -521,7 +521,7 @@ export const countries = { continent: "AF", capital: "Djibouti", currency: ["DJF"], - languages: ["fr", "ar"], + languages: ["fr", "ar"] }, DK: { name: "Denmark", @@ -531,7 +531,7 @@ export const countries = { continents: ["EU", "NA"], capital: "Copenhagen", currency: ["DKK"], - languages: ["da"], + languages: ["da"] }, DM: { name: "Dominica", @@ -540,7 +540,7 @@ export const countries = { continent: "NA", capital: "Roseau", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, DO: { name: "Dominican Republic", @@ -549,7 +549,7 @@ export const countries = { continent: "NA", capital: "Santo Domingo", currency: ["DOP"], - languages: ["es"], + languages: ["es"] }, DZ: { name: "Algeria", @@ -558,7 +558,7 @@ export const countries = { continent: "AF", capital: "Algiers", currency: ["DZD"], - languages: ["ar"], + languages: ["ar"] }, EC: { name: "Ecuador", @@ -567,7 +567,7 @@ export const countries = { continent: "SA", capital: "Quito", currency: ["USD"], - languages: ["es"], + languages: ["es"] }, EE: { name: "Estonia", @@ -576,7 +576,7 @@ export const countries = { continent: "EU", capital: "Tallinn", currency: ["EUR"], - languages: ["et"], + languages: ["et"] }, EG: { name: "Egypt", @@ -586,7 +586,7 @@ export const countries = { continents: ["AF", "AS"], capital: "Cairo", currency: ["EGP"], - languages: ["ar"], + languages: ["ar"] }, EH: { name: "Western Sahara", @@ -595,7 +595,7 @@ export const countries = { continent: "AF", capital: "El Aaiún", currency: ["MAD", "DZD", "MRU"], - languages: ["es"], + languages: ["es"] }, ER: { name: "Eritrea", @@ -604,7 +604,7 @@ export const countries = { continent: "AF", capital: "Asmara", currency: ["ERN"], - languages: ["ti", "ar", "en"], + languages: ["ti", "ar", "en"] }, ES: { name: "Spain", @@ -613,7 +613,7 @@ export const countries = { continent: "EU", capital: "Madrid", currency: ["EUR"], - languages: ["es", "eu", "ca", "gl", "oc"], + languages: ["es", "eu", "ca", "gl", "oc"] }, ET: { name: "Ethiopia", @@ -622,7 +622,7 @@ export const countries = { continent: "AF", capital: "Addis Ababa", currency: ["ETB"], - languages: ["am"], + languages: ["am"] }, FI: { name: "Finland", @@ -631,7 +631,7 @@ export const countries = { continent: "EU", capital: "Helsinki", currency: ["EUR"], - languages: ["fi", "sv"], + languages: ["fi", "sv"] }, FJ: { name: "Fiji", @@ -640,7 +640,7 @@ export const countries = { continent: "OC", capital: "Suva", currency: ["FJD"], - languages: ["en", "fj", "hi", "ur"], + languages: ["en", "fj", "hi", "ur"] }, FK: { name: "Falkland Islands", @@ -649,7 +649,7 @@ export const countries = { continent: "SA", capital: "Stanley", currency: ["FKP"], - languages: ["en"], + languages: ["en"] }, FM: { name: "Micronesia", @@ -658,7 +658,7 @@ export const countries = { continent: "OC", capital: "Palikir", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, FO: { name: "Faroe Islands", @@ -667,7 +667,7 @@ export const countries = { continent: "EU", capital: "Tórshavn", currency: ["DKK"], - languages: ["fo"], + languages: ["fo"] }, FR: { name: "France", @@ -676,7 +676,7 @@ export const countries = { continent: "EU", capital: "Paris", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, GA: { name: "Gabon", @@ -685,7 +685,7 @@ export const countries = { continent: "AF", capital: "Libreville", currency: ["XAF"], - languages: ["fr"], + languages: ["fr"] }, GB: { name: "United Kingdom", @@ -694,7 +694,7 @@ export const countries = { continent: "EU", capital: "London", currency: ["GBP"], - languages: ["en"], + languages: ["en"] }, GD: { name: "Grenada", @@ -703,7 +703,7 @@ export const countries = { continent: "NA", capital: "St. George's", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, GE: { name: "Georgia", @@ -713,7 +713,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Tbilisi", currency: ["GEL"], - languages: ["ka"], + languages: ["ka"] }, GF: { name: "French Guiana", @@ -722,7 +722,7 @@ export const countries = { continent: "SA", capital: "Cayenne", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, GG: { name: "Guernsey", @@ -731,7 +731,7 @@ export const countries = { continent: "EU", capital: "St. Peter Port", currency: ["GBP"], - languages: ["en", "fr"], + languages: ["en", "fr"] }, GH: { name: "Ghana", @@ -740,7 +740,7 @@ export const countries = { continent: "AF", capital: "Accra", currency: ["GHS"], - languages: ["en"], + languages: ["en"] }, GI: { name: "Gibraltar", @@ -749,7 +749,7 @@ export const countries = { continent: "EU", capital: "Gibraltar", currency: ["GIP"], - languages: ["en"], + languages: ["en"] }, GL: { name: "Greenland", @@ -758,7 +758,7 @@ export const countries = { continent: "NA", capital: "Nuuk", currency: ["DKK"], - languages: ["kl"], + languages: ["kl"] }, GM: { name: "Gambia", @@ -767,7 +767,7 @@ export const countries = { continent: "AF", capital: "Banjul", currency: ["GMD"], - languages: ["en"], + languages: ["en"] }, GN: { name: "Guinea", @@ -776,7 +776,7 @@ export const countries = { continent: "AF", capital: "Conakry", currency: ["GNF"], - languages: ["fr", "ff"], + languages: ["fr", "ff"] }, GP: { name: "Guadeloupe", @@ -785,7 +785,7 @@ export const countries = { continent: "NA", capital: "Basse-Terre", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, GQ: { name: "Equatorial Guinea", @@ -794,7 +794,7 @@ export const countries = { continent: "AF", capital: "Malabo", currency: ["XAF"], - languages: ["es", "fr"], + languages: ["es", "fr"] }, GR: { name: "Greece", @@ -803,7 +803,7 @@ export const countries = { continent: "EU", capital: "Athens", currency: ["EUR"], - languages: ["el"], + languages: ["el"] }, GS: { name: "South Georgia and the South Sandwich Islands", @@ -812,7 +812,7 @@ export const countries = { continent: "AN", capital: "King Edward Point", currency: ["GBP"], - languages: ["en"], + languages: ["en"] }, GT: { name: "Guatemala", @@ -821,7 +821,7 @@ export const countries = { continent: "NA", capital: "Guatemala City", currency: ["GTQ"], - languages: ["es"], + languages: ["es"] }, GU: { name: "Guam", @@ -830,7 +830,7 @@ export const countries = { continent: "OC", capital: "Hagåtña", currency: ["USD"], - languages: ["en", "ch", "es"], + languages: ["en", "ch", "es"] }, GW: { name: "Guinea-Bissau", @@ -839,7 +839,7 @@ export const countries = { continent: "AF", capital: "Bissau", currency: ["XOF"], - languages: ["pt"], + languages: ["pt"] }, GY: { name: "Guyana", @@ -848,7 +848,7 @@ export const countries = { continent: "SA", capital: "Georgetown", currency: ["GYD"], - languages: ["en"], + languages: ["en"] }, HK: { name: "Hong Kong", @@ -857,7 +857,7 @@ export const countries = { continent: "AS", capital: "City of Victoria", currency: ["HKD"], - languages: ["zh", "en"], + languages: ["zh", "en"] }, HM: { name: "Heard Island and McDonald Islands", @@ -866,7 +866,7 @@ export const countries = { continent: "AN", capital: "", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, HN: { name: "Honduras", @@ -875,7 +875,7 @@ export const countries = { continent: "NA", capital: "Tegucigalpa", currency: ["HNL"], - languages: ["es"], + languages: ["es"] }, HR: { name: "Croatia", @@ -884,7 +884,7 @@ export const countries = { continent: "EU", capital: "Zagreb", currency: ["EUR"], - languages: ["hr"], + languages: ["hr"] }, HT: { name: "Haiti", @@ -893,7 +893,7 @@ export const countries = { continent: "NA", capital: "Port-au-Prince", currency: ["HTG", "USD"], - languages: ["fr", "ht"], + languages: ["fr", "ht"] }, HU: { name: "Hungary", @@ -902,7 +902,7 @@ export const countries = { continent: "EU", capital: "Budapest", currency: ["HUF"], - languages: ["hu"], + languages: ["hu"] }, ID: { name: "Indonesia", @@ -911,7 +911,7 @@ export const countries = { continent: "AS", capital: "Jakarta", currency: ["IDR"], - languages: ["id"], + languages: ["id"] }, IE: { name: "Ireland", @@ -920,7 +920,7 @@ export const countries = { continent: "EU", capital: "Dublin", currency: ["EUR"], - languages: ["ga", "en"], + languages: ["ga", "en"] }, IL: { name: "Israel", @@ -929,7 +929,7 @@ export const countries = { continent: "AS", capital: "Jerusalem", currency: ["ILS"], - languages: ["he", "ar"], + languages: ["he", "ar"] }, IM: { name: "Isle of Man", @@ -938,7 +938,7 @@ export const countries = { continent: "EU", capital: "Douglas", currency: ["GBP"], - languages: ["en", "gv"], + languages: ["en", "gv"] }, IN: { name: "India", @@ -947,7 +947,7 @@ export const countries = { continent: "AS", capital: "New Delhi", currency: ["INR"], - languages: ["hi", "en"], + languages: ["hi", "en"] }, IO: { name: "British Indian Ocean Territory", @@ -956,7 +956,7 @@ export const countries = { continent: "AS", capital: "Diego Garcia", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, IQ: { name: "Iraq", @@ -965,7 +965,7 @@ export const countries = { continent: "AS", capital: "Baghdad", currency: ["IQD"], - languages: ["ar", "ku"], + languages: ["ar", "ku"] }, IR: { name: "Iran", @@ -974,7 +974,7 @@ export const countries = { continent: "AS", capital: "Tehran", currency: ["IRR"], - languages: ["fa"], + languages: ["fa"] }, IS: { name: "Iceland", @@ -983,7 +983,7 @@ export const countries = { continent: "EU", capital: "Reykjavik", currency: ["ISK"], - languages: ["is"], + languages: ["is"] }, IT: { name: "Italy", @@ -992,7 +992,7 @@ export const countries = { continent: "EU", capital: "Rome", currency: ["EUR"], - languages: ["it"], + languages: ["it"] }, JE: { name: "Jersey", @@ -1001,7 +1001,7 @@ export const countries = { continent: "EU", capital: "Saint Helier", currency: ["GBP"], - languages: ["en", "fr"], + languages: ["en", "fr"] }, JM: { name: "Jamaica", @@ -1010,7 +1010,7 @@ export const countries = { continent: "NA", capital: "Kingston", currency: ["JMD"], - languages: ["en"], + languages: ["en"] }, JO: { name: "Jordan", @@ -1019,7 +1019,7 @@ export const countries = { continent: "AS", capital: "Amman", currency: ["JOD"], - languages: ["ar"], + languages: ["ar"] }, JP: { name: "Japan", @@ -1028,7 +1028,7 @@ export const countries = { continent: "AS", capital: "Tokyo", currency: ["JPY"], - languages: ["ja"], + languages: ["ja"] }, KE: { name: "Kenya", @@ -1037,7 +1037,7 @@ export const countries = { continent: "AF", capital: "Nairobi", currency: ["KES"], - languages: ["en", "sw"], + languages: ["en", "sw"] }, KG: { name: "Kyrgyzstan", @@ -1046,7 +1046,7 @@ export const countries = { continent: "AS", capital: "Bishkek", currency: ["KGS"], - languages: ["ky", "ru"], + languages: ["ky", "ru"] }, KH: { name: "Cambodia", @@ -1055,7 +1055,7 @@ export const countries = { continent: "AS", capital: "Phnom Penh", currency: ["KHR"], - languages: ["km"], + languages: ["km"] }, KI: { name: "Kiribati", @@ -1064,7 +1064,7 @@ export const countries = { continent: "OC", capital: "South Tarawa", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, KM: { name: "Comoros", @@ -1073,7 +1073,7 @@ export const countries = { continent: "AF", capital: "Moroni", currency: ["KMF"], - languages: ["ar", "fr"], + languages: ["ar", "fr"] }, KN: { name: "Saint Kitts and Nevis", @@ -1082,7 +1082,7 @@ export const countries = { continent: "NA", capital: "Basseterre", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, KP: { name: "North Korea", @@ -1091,7 +1091,7 @@ export const countries = { continent: "AS", capital: "Pyongyang", currency: ["KPW"], - languages: ["ko"], + languages: ["ko"] }, KR: { name: "South Korea", @@ -1100,7 +1100,7 @@ export const countries = { continent: "AS", capital: "Seoul", currency: ["KRW"], - languages: ["ko"], + languages: ["ko"] }, KW: { name: "Kuwait", @@ -1109,7 +1109,7 @@ export const countries = { continent: "AS", capital: "Kuwait City", currency: ["KWD"], - languages: ["ar"], + languages: ["ar"] }, KY: { name: "Cayman Islands", @@ -1118,7 +1118,7 @@ export const countries = { continent: "NA", capital: "George Town", currency: ["KYD"], - languages: ["en"], + languages: ["en"] }, KZ: { name: "Kazakhstan", @@ -1128,7 +1128,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Astana", currency: ["KZT"], - languages: ["kk", "ru"], + languages: ["kk", "ru"] }, LA: { name: "Laos", @@ -1137,7 +1137,7 @@ export const countries = { continent: "AS", capital: "Vientiane", currency: ["LAK"], - languages: ["lo"], + languages: ["lo"] }, LB: { name: "Lebanon", @@ -1146,7 +1146,7 @@ export const countries = { continent: "AS", capital: "Beirut", currency: ["LBP"], - languages: ["ar", "fr"], + languages: ["ar", "fr"] }, LC: { name: "Saint Lucia", @@ -1155,7 +1155,7 @@ export const countries = { continent: "NA", capital: "Castries", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, LI: { name: "Liechtenstein", @@ -1164,7 +1164,7 @@ export const countries = { continent: "EU", capital: "Vaduz", currency: ["CHF"], - languages: ["de"], + languages: ["de"] }, LK: { name: "Sri Lanka", @@ -1173,7 +1173,7 @@ export const countries = { continent: "AS", capital: "Colombo", currency: ["LKR"], - languages: ["si", "ta"], + languages: ["si", "ta"] }, LR: { name: "Liberia", @@ -1182,7 +1182,7 @@ export const countries = { continent: "AF", capital: "Monrovia", currency: ["LRD"], - languages: ["en"], + languages: ["en"] }, LS: { name: "Lesotho", @@ -1191,7 +1191,7 @@ export const countries = { continent: "AF", capital: "Maseru", currency: ["LSL", "ZAR"], - languages: ["en", "st"], + languages: ["en", "st"] }, LT: { name: "Lithuania", @@ -1200,7 +1200,7 @@ export const countries = { continent: "EU", capital: "Vilnius", currency: ["EUR"], - languages: ["lt"], + languages: ["lt"] }, LU: { name: "Luxembourg", @@ -1209,7 +1209,7 @@ export const countries = { continent: "EU", capital: "Luxembourg", currency: ["EUR"], - languages: ["fr", "de", "lb"], + languages: ["fr", "de", "lb"] }, LV: { name: "Latvia", @@ -1218,7 +1218,7 @@ export const countries = { continent: "EU", capital: "Riga", currency: ["EUR"], - languages: ["lv"], + languages: ["lv"] }, LY: { name: "Libya", @@ -1227,7 +1227,7 @@ export const countries = { continent: "AF", capital: "Tripoli", currency: ["LYD"], - languages: ["ar"], + languages: ["ar"] }, MA: { name: "Morocco", @@ -1236,7 +1236,7 @@ export const countries = { continent: "AF", capital: "Rabat", currency: ["MAD"], - languages: ["ar"], + languages: ["ar"] }, MC: { name: "Monaco", @@ -1245,7 +1245,7 @@ export const countries = { continent: "EU", capital: "Monaco", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, MD: { name: "Moldova", @@ -1254,7 +1254,7 @@ export const countries = { continent: "EU", capital: "Chișinău", currency: ["MDL"], - languages: ["ro"], + languages: ["ro"] }, ME: { name: "Montenegro", @@ -1263,7 +1263,7 @@ export const countries = { continent: "EU", capital: "Podgorica", currency: ["EUR"], - languages: ["sr", "bs", "sq", "hr"], + languages: ["sr", "bs", "sq", "hr"] }, MF: { name: "Saint Martin", @@ -1272,7 +1272,7 @@ export const countries = { continent: "NA", capital: "Marigot", currency: ["EUR"], - languages: ["en", "fr", "nl"], + languages: ["en", "fr", "nl"] }, MG: { name: "Madagascar", @@ -1281,7 +1281,7 @@ export const countries = { continent: "AF", capital: "Antananarivo", currency: ["MGA"], - languages: ["fr", "mg"], + languages: ["fr", "mg"] }, MH: { name: "Marshall Islands", @@ -1290,7 +1290,7 @@ export const countries = { continent: "OC", capital: "Majuro", currency: ["USD"], - languages: ["en", "mh"], + languages: ["en", "mh"] }, MK: { name: "North Macedonia", @@ -1299,7 +1299,7 @@ export const countries = { continent: "EU", capital: "Skopje", currency: ["MKD"], - languages: ["mk"], + languages: ["mk"] }, ML: { name: "Mali", @@ -1308,7 +1308,7 @@ export const countries = { continent: "AF", capital: "Bamako", currency: ["XOF"], - languages: ["fr"], + languages: ["fr"] }, MM: { name: "Myanmar (Burma)", @@ -1317,7 +1317,7 @@ export const countries = { continent: "AS", capital: "Naypyidaw", currency: ["MMK"], - languages: ["my"], + languages: ["my"] }, MN: { name: "Mongolia", @@ -1326,7 +1326,7 @@ export const countries = { continent: "AS", capital: "Ulan Bator", currency: ["MNT"], - languages: ["mn"], + languages: ["mn"] }, MO: { name: "Macao", @@ -1335,7 +1335,7 @@ export const countries = { continent: "AS", capital: "", currency: ["MOP"], - languages: ["zh", "pt"], + languages: ["zh", "pt"] }, MP: { name: "Northern Mariana Islands", @@ -1344,7 +1344,7 @@ export const countries = { continent: "OC", capital: "Saipan", currency: ["USD"], - languages: ["en", "ch"], + languages: ["en", "ch"] }, MQ: { name: "Martinique", @@ -1353,7 +1353,7 @@ export const countries = { continent: "NA", capital: "Fort-de-France", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, MR: { name: "Mauritania", @@ -1362,7 +1362,7 @@ export const countries = { continent: "AF", capital: "Nouakchott", currency: ["MRU"], - languages: ["ar"], + languages: ["ar"] }, MS: { name: "Montserrat", @@ -1371,7 +1371,7 @@ export const countries = { continent: "NA", capital: "Plymouth", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, MT: { name: "Malta", @@ -1380,7 +1380,7 @@ export const countries = { continent: "EU", capital: "Valletta", currency: ["EUR"], - languages: ["mt", "en"], + languages: ["mt", "en"] }, MU: { name: "Mauritius", @@ -1389,7 +1389,7 @@ export const countries = { continent: "AF", capital: "Port Louis", currency: ["MUR"], - languages: ["en"], + languages: ["en"] }, MV: { name: "Maldives", @@ -1398,7 +1398,7 @@ export const countries = { continent: "AS", capital: "Malé", currency: ["MVR"], - languages: ["dv"], + languages: ["dv"] }, MW: { name: "Malawi", @@ -1407,7 +1407,7 @@ export const countries = { continent: "AF", capital: "Lilongwe", currency: ["MWK"], - languages: ["en", "ny"], + languages: ["en", "ny"] }, MX: { name: "Mexico", @@ -1416,7 +1416,7 @@ export const countries = { continent: "NA", capital: "Mexico City", currency: ["MXN"], - languages: ["es"], + languages: ["es"] }, MY: { name: "Malaysia", @@ -1425,7 +1425,7 @@ export const countries = { continent: "AS", capital: "Kuala Lumpur", currency: ["MYR"], - languages: ["ms"], + languages: ["ms"] }, MZ: { name: "Mozambique", @@ -1434,7 +1434,7 @@ export const countries = { continent: "AF", capital: "Maputo", currency: ["MZN"], - languages: ["pt"], + languages: ["pt"] }, NA: { name: "Namibia", @@ -1443,7 +1443,7 @@ export const countries = { continent: "AF", capital: "Windhoek", currency: ["NAD", "ZAR"], - languages: ["en", "af"], + languages: ["en", "af"] }, NC: { name: "New Caledonia", @@ -1452,7 +1452,7 @@ export const countries = { continent: "OC", capital: "Nouméa", currency: ["XPF"], - languages: ["fr"], + languages: ["fr"] }, NE: { name: "Niger", @@ -1461,7 +1461,7 @@ export const countries = { continent: "AF", capital: "Niamey", currency: ["XOF"], - languages: ["fr"], + languages: ["fr"] }, NF: { name: "Norfolk Island", @@ -1470,7 +1470,7 @@ export const countries = { continent: "OC", capital: "Kingston", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, NG: { name: "Nigeria", @@ -1479,7 +1479,7 @@ export const countries = { continent: "AF", capital: "Abuja", currency: ["NGN"], - languages: ["en"], + languages: ["en"] }, NI: { name: "Nicaragua", @@ -1488,7 +1488,7 @@ export const countries = { continent: "NA", capital: "Managua", currency: ["NIO"], - languages: ["es"], + languages: ["es"] }, NL: { name: "Netherlands", @@ -1497,7 +1497,7 @@ export const countries = { continent: "EU", capital: "Amsterdam", currency: ["EUR"], - languages: ["nl"], + languages: ["nl"] }, NO: { name: "Norway", @@ -1506,7 +1506,7 @@ export const countries = { continent: "EU", capital: "Oslo", currency: ["NOK"], - languages: ["no", "nb", "nn"], + languages: ["no", "nb", "nn"] }, NP: { name: "Nepal", @@ -1515,7 +1515,7 @@ export const countries = { continent: "AS", capital: "Kathmandu", currency: ["NPR"], - languages: ["ne"], + languages: ["ne"] }, NR: { name: "Nauru", @@ -1524,7 +1524,7 @@ export const countries = { continent: "OC", capital: "Yaren", currency: ["AUD"], - languages: ["en", "na"], + languages: ["en", "na"] }, NU: { name: "Niue", @@ -1533,7 +1533,7 @@ export const countries = { continent: "OC", capital: "Alofi", currency: ["NZD"], - languages: ["en"], + languages: ["en"] }, NZ: { name: "New Zealand", @@ -1542,7 +1542,7 @@ export const countries = { continent: "OC", capital: "Wellington", currency: ["NZD"], - languages: ["en", "mi"], + languages: ["en", "mi"] }, OM: { name: "Oman", @@ -1551,7 +1551,7 @@ export const countries = { continent: "AS", capital: "Muscat", currency: ["OMR"], - languages: ["ar"], + languages: ["ar"] }, PA: { name: "Panama", @@ -1560,7 +1560,7 @@ export const countries = { continent: "NA", capital: "Panama City", currency: ["PAB", "USD"], - languages: ["es"], + languages: ["es"] }, PE: { name: "Peru", @@ -1569,7 +1569,7 @@ export const countries = { continent: "SA", capital: "Lima", currency: ["PEN"], - languages: ["es"], + languages: ["es"] }, PF: { name: "French Polynesia", @@ -1578,7 +1578,7 @@ export const countries = { continent: "OC", capital: "Papeetē", currency: ["XPF"], - languages: ["fr"], + languages: ["fr"] }, PG: { name: "Papua New Guinea", @@ -1587,7 +1587,7 @@ export const countries = { continent: "OC", capital: "Port Moresby", currency: ["PGK"], - languages: ["en"], + languages: ["en"] }, PH: { name: "Philippines", @@ -1596,7 +1596,7 @@ export const countries = { continent: "AS", capital: "Manila", currency: ["PHP"], - languages: ["en"], + languages: ["en"] }, PK: { name: "Pakistan", @@ -1605,7 +1605,7 @@ export const countries = { continent: "AS", capital: "Islamabad", currency: ["PKR"], - languages: ["en", "ur"], + languages: ["en", "ur"] }, PL: { name: "Poland", @@ -1614,7 +1614,7 @@ export const countries = { continent: "EU", capital: "Warsaw", currency: ["PLN"], - languages: ["pl"], + languages: ["pl"] }, PM: { name: "Saint Pierre and Miquelon", @@ -1623,7 +1623,7 @@ export const countries = { continent: "NA", capital: "Saint-Pierre", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, PN: { name: "Pitcairn Islands", @@ -1632,7 +1632,7 @@ export const countries = { continent: "OC", capital: "Adamstown", currency: ["NZD"], - languages: ["en"], + languages: ["en"] }, PR: { name: "Puerto Rico", @@ -1641,7 +1641,7 @@ export const countries = { continent: "NA", capital: "San Juan", currency: ["USD"], - languages: ["es", "en"], + languages: ["es", "en"] }, PS: { name: "Palestine", @@ -1650,7 +1650,7 @@ export const countries = { continent: "AS", capital: "Ramallah", currency: ["ILS"], - languages: ["ar"], + languages: ["ar"] }, PT: { name: "Portugal", @@ -1659,7 +1659,7 @@ export const countries = { continent: "EU", capital: "Lisbon", currency: ["EUR"], - languages: ["pt"], + languages: ["pt"] }, PW: { name: "Palau", @@ -1668,7 +1668,7 @@ export const countries = { continent: "OC", capital: "Ngerulmud", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, PY: { name: "Paraguay", @@ -1677,7 +1677,7 @@ export const countries = { continent: "SA", capital: "Asunción", currency: ["PYG"], - languages: ["es", "gn"], + languages: ["es", "gn"] }, QA: { name: "Qatar", @@ -1686,7 +1686,7 @@ export const countries = { continent: "AS", capital: "Doha", currency: ["QAR"], - languages: ["ar"], + languages: ["ar"] }, RE: { name: "Reunion", @@ -1695,7 +1695,7 @@ export const countries = { continent: "AF", capital: "Saint-Denis", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, RO: { name: "Romania", @@ -1704,7 +1704,7 @@ export const countries = { continent: "EU", capital: "Bucharest", currency: ["RON"], - languages: ["ro"], + languages: ["ro"] }, RS: { name: "Serbia", @@ -1713,7 +1713,7 @@ export const countries = { continent: "EU", capital: "Belgrade", currency: ["RSD"], - languages: ["sr"], + languages: ["sr"] }, RU: { name: "Russia", @@ -1723,7 +1723,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Moscow", currency: ["RUB"], - languages: ["ru"], + languages: ["ru"] }, RW: { name: "Rwanda", @@ -1732,7 +1732,7 @@ export const countries = { continent: "AF", capital: "Kigali", currency: ["RWF"], - languages: ["rw", "en", "fr"], + languages: ["rw", "en", "fr"] }, SA: { name: "Saudi Arabia", @@ -1741,7 +1741,7 @@ export const countries = { continent: "AS", capital: "Riyadh", currency: ["SAR"], - languages: ["ar"], + languages: ["ar"] }, SB: { name: "Solomon Islands", @@ -1750,7 +1750,7 @@ export const countries = { continent: "OC", capital: "Honiara", currency: ["SBD"], - languages: ["en"], + languages: ["en"] }, SC: { name: "Seychelles", @@ -1759,7 +1759,7 @@ export const countries = { continent: "AF", capital: "Victoria", currency: ["SCR"], - languages: ["fr", "en"], + languages: ["fr", "en"] }, SD: { name: "Sudan", @@ -1768,7 +1768,7 @@ export const countries = { continent: "AF", capital: "Khartoum", currency: ["SDG"], - languages: ["ar", "en"], + languages: ["ar", "en"] }, SE: { name: "Sweden", @@ -1777,7 +1777,7 @@ export const countries = { continent: "EU", capital: "Stockholm", currency: ["SEK"], - languages: ["sv"], + languages: ["sv"] }, SG: { name: "Singapore", @@ -1786,7 +1786,7 @@ export const countries = { continent: "AS", capital: "Singapore", currency: ["SGD"], - languages: ["en", "ms", "ta", "zh"], + languages: ["en", "ms", "ta", "zh"] }, SH: { name: "Saint Helena", @@ -1795,7 +1795,7 @@ export const countries = { continent: "AF", capital: "Jamestown", currency: ["SHP"], - languages: ["en"], + languages: ["en"] }, SI: { name: "Slovenia", @@ -1804,7 +1804,7 @@ export const countries = { continent: "EU", capital: "Ljubljana", currency: ["EUR"], - languages: ["sl"], + languages: ["sl"] }, SJ: { name: "Svalbard and Jan Mayen", @@ -1813,7 +1813,7 @@ export const countries = { continent: "EU", capital: "Longyearbyen", currency: ["NOK"], - languages: ["no"], + languages: ["no"] }, SK: { name: "Slovakia", @@ -1822,7 +1822,7 @@ export const countries = { continent: "EU", capital: "Bratislava", currency: ["EUR"], - languages: ["sk"], + languages: ["sk"] }, SL: { name: "Sierra Leone", @@ -1831,7 +1831,7 @@ export const countries = { continent: "AF", capital: "Freetown", currency: ["SLL"], - languages: ["en"], + languages: ["en"] }, SM: { name: "San Marino", @@ -1840,7 +1840,7 @@ export const countries = { continent: "EU", capital: "City of San Marino", currency: ["EUR"], - languages: ["it"], + languages: ["it"] }, SN: { name: "Senegal", @@ -1849,7 +1849,7 @@ export const countries = { continent: "AF", capital: "Dakar", currency: ["XOF"], - languages: ["fr"], + languages: ["fr"] }, SO: { name: "Somalia", @@ -1858,7 +1858,7 @@ export const countries = { continent: "AF", capital: "Mogadishu", currency: ["SOS"], - languages: ["so", "ar"], + languages: ["so", "ar"] }, SR: { name: "Suriname", @@ -1867,7 +1867,7 @@ export const countries = { continent: "SA", capital: "Paramaribo", currency: ["SRD"], - languages: ["nl"], + languages: ["nl"] }, SS: { name: "South Sudan", @@ -1876,7 +1876,7 @@ export const countries = { continent: "AF", capital: "Juba", currency: ["SSP"], - languages: ["en"], + languages: ["en"] }, ST: { name: "Sao Tome and Principe", @@ -1885,7 +1885,7 @@ export const countries = { continent: "AF", capital: "São Tomé", currency: ["STN"], - languages: ["pt"], + languages: ["pt"] }, SV: { name: "El Salvador", @@ -1894,7 +1894,7 @@ export const countries = { continent: "NA", capital: "San Salvador", currency: ["SVC", "USD"], - languages: ["es"], + languages: ["es"] }, SX: { name: "Sint Maarten", @@ -1903,7 +1903,7 @@ export const countries = { continent: "NA", capital: "Philipsburg", currency: ["ANG"], - languages: ["nl", "en"], + languages: ["nl", "en"] }, SY: { name: "Syria", @@ -1912,7 +1912,7 @@ export const countries = { continent: "AS", capital: "Damascus", currency: ["SYP"], - languages: ["ar"], + languages: ["ar"] }, SZ: { name: "Eswatini", @@ -1921,7 +1921,7 @@ export const countries = { continent: "AF", capital: "Lobamba", currency: ["SZL"], - languages: ["en", "ss"], + languages: ["en", "ss"] }, TC: { name: "Turks and Caicos Islands", @@ -1930,7 +1930,7 @@ export const countries = { continent: "NA", capital: "Cockburn Town", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, TD: { name: "Chad", @@ -1939,7 +1939,7 @@ export const countries = { continent: "AF", capital: "N'Djamena", currency: ["XAF"], - languages: ["fr", "ar"], + languages: ["fr", "ar"] }, TF: { name: "French Southern Territories", @@ -1948,7 +1948,7 @@ export const countries = { continent: "AN", capital: "Port-aux-Français", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, TG: { name: "Togo", @@ -1957,7 +1957,7 @@ export const countries = { continent: "AF", capital: "Lomé", currency: ["XOF"], - languages: ["fr"], + languages: ["fr"] }, TH: { name: "Thailand", @@ -1966,7 +1966,7 @@ export const countries = { continent: "AS", capital: "Bangkok", currency: ["THB"], - languages: ["th"], + languages: ["th"] }, TJ: { name: "Tajikistan", @@ -1975,7 +1975,7 @@ export const countries = { continent: "AS", capital: "Dushanbe", currency: ["TJS"], - languages: ["tg", "ru"], + languages: ["tg", "ru"] }, TK: { name: "Tokelau", @@ -1984,7 +1984,7 @@ export const countries = { continent: "OC", capital: "Fakaofo", currency: ["NZD"], - languages: ["en"], + languages: ["en"] }, TL: { name: "East Timor", @@ -1993,7 +1993,7 @@ export const countries = { continent: "OC", capital: "Dili", currency: ["USD"], - languages: ["pt"], + languages: ["pt"] }, TM: { name: "Turkmenistan", @@ -2002,7 +2002,7 @@ export const countries = { continent: "AS", capital: "Ashgabat", currency: ["TMT"], - languages: ["tk", "ru"], + languages: ["tk", "ru"] }, TN: { name: "Tunisia", @@ -2011,7 +2011,7 @@ export const countries = { continent: "AF", capital: "Tunis", currency: ["TND"], - languages: ["ar"], + languages: ["ar"] }, TO: { name: "Tonga", @@ -2020,7 +2020,7 @@ export const countries = { continent: "OC", capital: "Nuku'alofa", currency: ["TOP"], - languages: ["en", "to"], + languages: ["en", "to"] }, TR: { name: "Turkey", @@ -2030,7 +2030,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Ankara", currency: ["TRY"], - languages: ["tr"], + languages: ["tr"] }, TT: { name: "Trinidad and Tobago", @@ -2039,7 +2039,7 @@ export const countries = { continent: "NA", capital: "Port of Spain", currency: ["TTD"], - languages: ["en"], + languages: ["en"] }, TV: { name: "Tuvalu", @@ -2048,7 +2048,7 @@ export const countries = { continent: "OC", capital: "Funafuti", currency: ["AUD"], - languages: ["en"], + languages: ["en"] }, TW: { name: "Taiwan", @@ -2057,7 +2057,7 @@ export const countries = { continent: "AS", capital: "Taipei", currency: ["TWD"], - languages: ["zh"], + languages: ["zh"] }, TZ: { name: "Tanzania", @@ -2066,7 +2066,7 @@ export const countries = { continent: "AF", capital: "Dodoma", currency: ["TZS"], - languages: ["sw", "en"], + languages: ["sw", "en"] }, UA: { name: "Ukraine", @@ -2075,7 +2075,7 @@ export const countries = { continent: "EU", capital: "Kyiv", currency: ["UAH"], - languages: ["uk"], + languages: ["uk"] }, UG: { name: "Uganda", @@ -2084,7 +2084,7 @@ export const countries = { continent: "AF", capital: "Kampala", currency: ["UGX"], - languages: ["en", "sw"], + languages: ["en", "sw"] }, UM: { name: "U.S. Minor Outlying Islands", @@ -2093,7 +2093,7 @@ export const countries = { continent: "OC", capital: "", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, US: { name: "United States", @@ -2102,7 +2102,7 @@ export const countries = { continent: "NA", capital: "Washington D.C.", currency: ["USD", "USN", "USS"], - languages: ["en"], + languages: ["en"] }, UY: { name: "Uruguay", @@ -2111,7 +2111,7 @@ export const countries = { continent: "SA", capital: "Montevideo", currency: ["UYI", "UYU"], - languages: ["es"], + languages: ["es"] }, UZ: { name: "Uzbekistan", @@ -2120,7 +2120,7 @@ export const countries = { continent: "AS", capital: "Tashkent", currency: ["UZS"], - languages: ["uz", "ru"], + languages: ["uz", "ru"] }, VA: { name: "Vatican City", @@ -2129,7 +2129,7 @@ export const countries = { continent: "EU", capital: "Vatican City", currency: ["EUR"], - languages: ["it", "la"], + languages: ["it", "la"] }, VC: { name: "Saint Vincent and the Grenadines", @@ -2138,7 +2138,7 @@ export const countries = { continent: "NA", capital: "Kingstown", currency: ["XCD"], - languages: ["en"], + languages: ["en"] }, VE: { name: "Venezuela", @@ -2147,7 +2147,7 @@ export const countries = { continent: "SA", capital: "Caracas", currency: ["VES"], - languages: ["es"], + languages: ["es"] }, VG: { name: "British Virgin Islands", @@ -2156,7 +2156,7 @@ export const countries = { continent: "NA", capital: "Road Town", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, VI: { name: "U.S. Virgin Islands", @@ -2165,7 +2165,7 @@ export const countries = { continent: "NA", capital: "Charlotte Amalie", currency: ["USD"], - languages: ["en"], + languages: ["en"] }, VN: { name: "Vietnam", @@ -2174,7 +2174,7 @@ export const countries = { continent: "AS", capital: "Hanoi", currency: ["VND"], - languages: ["vi"], + languages: ["vi"] }, VU: { name: "Vanuatu", @@ -2183,7 +2183,7 @@ export const countries = { continent: "OC", capital: "Port Vila", currency: ["VUV"], - languages: ["bi", "en", "fr"], + languages: ["bi", "en", "fr"] }, WF: { name: "Wallis and Futuna", @@ -2192,7 +2192,7 @@ export const countries = { continent: "OC", capital: "Mata-Utu", currency: ["XPF"], - languages: ["fr"], + languages: ["fr"] }, WS: { name: "Samoa", @@ -2201,7 +2201,7 @@ export const countries = { continent: "OC", capital: "Apia", currency: ["WST"], - languages: ["sm", "en"], + languages: ["sm", "en"] }, XK: { name: "Kosovo", @@ -2211,7 +2211,7 @@ export const countries = { capital: "Pristina", currency: ["EUR"], languages: ["sq", "sr"], - userAssigned: true, + userAssigned: true }, YE: { name: "Yemen", @@ -2220,7 +2220,7 @@ export const countries = { continent: "AS", capital: "Sana'a", currency: ["YER"], - languages: ["ar"], + languages: ["ar"] }, YT: { name: "Mayotte", @@ -2229,7 +2229,7 @@ export const countries = { continent: "AF", capital: "Mamoudzou", currency: ["EUR"], - languages: ["fr"], + languages: ["fr"] }, ZA: { name: "South Africa", @@ -2238,7 +2238,7 @@ export const countries = { continent: "AF", capital: "Pretoria", currency: ["ZAR"], - languages: ["af", "en", "nr", "st", "ss", "tn", "ts", "ve", "xh", "zu"], + languages: ["af", "en", "nr", "st", "ss", "tn", "ts", "ve", "xh", "zu"] }, ZM: { name: "Zambia", @@ -2247,7 +2247,7 @@ export const countries = { continent: "AF", capital: "Lusaka", currency: ["ZMW"], - languages: ["en"], + languages: ["en"] }, ZW: { name: "Zimbabwe", @@ -2256,6 +2256,6 @@ export const countries = { continent: "AF", capital: "Harare", currency: ["USD", "ZAR", "BWP", "GBP", "AUD", "CNY", "INR", "JPY"], - languages: ["en", "sn", "nd"], - }, + languages: ["en", "sn", "nd"] + } }; diff --git a/apps/api/src/lib/validateUrl.test.ts b/apps/api/src/lib/validateUrl.test.ts index eec39f97..81c150fb 100644 --- a/apps/api/src/lib/validateUrl.test.ts +++ b/apps/api/src/lib/validateUrl.test.ts @@ -18,7 +18,10 @@ describe("isSameDomain", () => { }); it("should return true for a subdomain with different protocols", () => { - const result = isSameDomain("https://sub.example.com", "http://example.com"); + const result = isSameDomain( + "https://sub.example.com", + "http://example.com" + ); expect(result).toBe(true); }); @@ -30,32 +33,44 @@ describe("isSameDomain", () => { }); it("should return true for a subdomain with www prefix", () => { - const result = isSameDomain("http://www.sub.example.com", "http://example.com"); + const result = isSameDomain( + "http://www.sub.example.com", + "http://example.com" + ); expect(result).toBe(true); }); it("should return true for the same domain with www prefix", () => { - const result = isSameDomain("http://docs.s.s.example.com", "http://example.com"); + const result = isSameDomain( + "http://docs.s.s.example.com", + "http://example.com" + ); expect(result).toBe(true); }); }); - - - describe("isSameSubdomain", () => { it("should return false for a subdomain", () => { - const result = isSameSubdomain("http://example.com", "http://docs.example.com"); + const result = isSameSubdomain( + "http://example.com", + "http://docs.example.com" + ); expect(result).toBe(false); }); it("should return true for the same subdomain", () => { - const result = isSameSubdomain("http://docs.example.com", "http://docs.example.com"); + const result = isSameSubdomain( + "http://docs.example.com", + "http://docs.example.com" + ); expect(result).toBe(true); }); it("should return false for different subdomains", () => { - const result = isSameSubdomain("http://docs.example.com", "http://blog.example.com"); + const result = isSameSubdomain( + "http://docs.example.com", + "http://blog.example.com" + ); expect(result).toBe(false); }); @@ -72,17 +87,26 @@ describe("isSameSubdomain", () => { }); it("should return true for the same subdomain with different protocols", () => { - const result = isSameSubdomain("https://docs.example.com", "http://docs.example.com"); + const result = isSameSubdomain( + "https://docs.example.com", + "http://docs.example.com" + ); expect(result).toBe(true); }); it("should return true for the same subdomain with www prefix", () => { - const result = isSameSubdomain("http://www.docs.example.com", "http://docs.example.com"); + const result = isSameSubdomain( + "http://www.docs.example.com", + "http://docs.example.com" + ); expect(result).toBe(true); }); it("should return false for a subdomain with www prefix and different subdomain", () => { - const result = isSameSubdomain("http://www.docs.example.com", "http://blog.example.com"); + const result = isSameSubdomain( + "http://www.docs.example.com", + "http://blog.example.com" + ); expect(result).toBe(false); }); }); @@ -116,19 +140,13 @@ describe("removeDuplicateUrls", () => { }); it("should prefer https over http", () => { - const urls = [ - "http://example.com", - "https://example.com" - ]; + const urls = ["http://example.com", "https://example.com"]; const result = removeDuplicateUrls(urls); expect(result).toEqual(["https://example.com"]); }); it("should prefer non-www over www", () => { - const urls = [ - "https://www.example.com", - "https://example.com" - ]; + const urls = ["https://www.example.com", "https://example.com"]; const result = removeDuplicateUrls(urls); expect(result).toEqual(["https://example.com"]); }); @@ -140,19 +158,13 @@ describe("removeDuplicateUrls", () => { }); it("should handle URLs with different cases", () => { - const urls = [ - "https://EXAMPLE.com", - "https://example.com" - ]; + const urls = ["https://EXAMPLE.com", "https://example.com"]; const result = removeDuplicateUrls(urls); expect(result).toEqual(["https://EXAMPLE.com"]); }); it("should handle URLs with trailing slashes", () => { - const urls = [ - "https://example.com", - "https://example.com/" - ]; + const urls = ["https://example.com", "https://example.com/"]; const result = removeDuplicateUrls(urls); expect(result).toEqual(["https://example.com"]); }); diff --git a/apps/api/src/lib/validateUrl.ts b/apps/api/src/lib/validateUrl.ts index 14a74de8..dc27c136 100644 --- a/apps/api/src/lib/validateUrl.ts +++ b/apps/api/src/lib/validateUrl.ts @@ -58,9 +58,9 @@ export const checkUrl = (url: string) => { * Same domain check * It checks if the domain of the url is the same as the base url * It accounts true for subdomains and www.subdomains - * @param url - * @param baseUrl - * @returns + * @param url + * @param baseUrl + * @returns */ export function isSameDomain(url: string, baseUrl: string) { const { urlObj: urlObj1, error: error1 } = getURLobj(url); @@ -74,16 +74,21 @@ export function isSameDomain(url: string, baseUrl: string) { const typedUrlObj2 = urlObj2 as URL; const cleanHostname = (hostname: string) => { - return hostname.startsWith('www.') ? hostname.slice(4) : hostname; + return hostname.startsWith("www.") ? hostname.slice(4) : hostname; }; - const domain1 = cleanHostname(typedUrlObj1.hostname).split('.').slice(-2).join('.'); - const domain2 = cleanHostname(typedUrlObj2.hostname).split('.').slice(-2).join('.'); + const domain1 = cleanHostname(typedUrlObj1.hostname) + .split(".") + .slice(-2) + .join("."); + const domain2 = cleanHostname(typedUrlObj2.hostname) + .split(".") + .slice(-2) + .join("."); return domain1 === domain2; } - export function isSameSubdomain(url: string, baseUrl: string) { const { urlObj: urlObj1, error: error1 } = getURLobj(url); const { urlObj: urlObj2, error: error2 } = getURLobj(baseUrl); @@ -96,20 +101,31 @@ export function isSameSubdomain(url: string, baseUrl: string) { const typedUrlObj2 = urlObj2 as URL; const cleanHostname = (hostname: string) => { - return hostname.startsWith('www.') ? hostname.slice(4) : hostname; + return hostname.startsWith("www.") ? hostname.slice(4) : hostname; }; - const domain1 = cleanHostname(typedUrlObj1.hostname).split('.').slice(-2).join('.'); - const domain2 = cleanHostname(typedUrlObj2.hostname).split('.').slice(-2).join('.'); + const domain1 = cleanHostname(typedUrlObj1.hostname) + .split(".") + .slice(-2) + .join("."); + const domain2 = cleanHostname(typedUrlObj2.hostname) + .split(".") + .slice(-2) + .join("."); - const subdomain1 = cleanHostname(typedUrlObj1.hostname).split('.').slice(0, -2).join('.'); - const subdomain2 = cleanHostname(typedUrlObj2.hostname).split('.').slice(0, -2).join('.'); + const subdomain1 = cleanHostname(typedUrlObj1.hostname) + .split(".") + .slice(0, -2) + .join("."); + const subdomain2 = cleanHostname(typedUrlObj2.hostname) + .split(".") + .slice(0, -2) + .join("."); // Check if the domains are the same and the subdomains are the same return domain1 === domain2 && subdomain1 === subdomain2; } - export const checkAndUpdateURLForMap = (url: string) => { if (!protocolIncluded(url)) { url = `http://${url}`; @@ -119,7 +135,6 @@ export const checkAndUpdateURLForMap = (url: string) => { url = url.slice(0, -1); } - const { error, urlObj } = getURLobj(url); if (error) { throw new Error("Invalid URL"); @@ -137,34 +152,34 @@ export const checkAndUpdateURLForMap = (url: string) => { return { urlObj: typedUrlObj, url: url }; }; - - - - export function removeDuplicateUrls(urls: string[]): string[] { const urlMap = new Map(); for (const url of urls) { const parsedUrl = new URL(url); const protocol = parsedUrl.protocol; - const hostname = parsedUrl.hostname.replace(/^www\./, ''); + const hostname = parsedUrl.hostname.replace(/^www\./, ""); const path = parsedUrl.pathname + parsedUrl.search + parsedUrl.hash; - + const key = `${hostname}${path}`; - + if (!urlMap.has(key)) { urlMap.set(key, url); } else { const existingUrl = new URL(urlMap.get(key)!); const existingProtocol = existingUrl.protocol; - - if (protocol === 'https:' && existingProtocol === 'http:') { + + if (protocol === "https:" && existingProtocol === "http:") { urlMap.set(key, url); - } else if (protocol === existingProtocol && !parsedUrl.hostname.startsWith('www.') && existingUrl.hostname.startsWith('www.')) { + } else if ( + protocol === existingProtocol && + !parsedUrl.hostname.startsWith("www.") && + existingUrl.hostname.startsWith("www.") + ) { urlMap.set(key, url); } } } return [...new Set(Array.from(urlMap.values()))]; -} \ No newline at end of file +} diff --git a/apps/api/src/lib/withAuth.ts b/apps/api/src/lib/withAuth.ts index a6cd539d..ab3f4d4b 100644 --- a/apps/api/src/lib/withAuth.ts +++ b/apps/api/src/lib/withAuth.ts @@ -8,10 +8,10 @@ let warningCount = 0; export function withAuth( originalFunction: (...args: U) => Promise, - mockSuccess: T, + mockSuccess: T ) { return async function (...args: U): Promise { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (!useDbAuthentication) { if (warningCount < 5) { logger.warn("You're bypassing authentication"); diff --git a/apps/api/src/main/runWebScraper.ts b/apps/api/src/main/runWebScraper.ts index 90d4a47f..981189ab 100644 --- a/apps/api/src/main/runWebScraper.ts +++ b/apps/api/src/main/runWebScraper.ts @@ -2,7 +2,7 @@ import { Job } from "bullmq"; import { WebScraperOptions, RunWebScraperParams, - RunWebScraperResult, + RunWebScraperResult } from "../types"; import { billTeam } from "../services/billing/credit_billing"; import { Document } from "../controllers/v1/types"; @@ -10,25 +10,31 @@ import { supabase_service } from "../services/supabase"; import { logger } from "../lib/logger"; import { ScrapeEvents } from "../lib/scrape-events"; import { configDotenv } from "dotenv"; -import { EngineResultsTracker, scrapeURL, ScrapeUrlResponse } from "../scraper/scrapeURL"; +import { + EngineResultsTracker, + scrapeURL, + ScrapeUrlResponse +} from "../scraper/scrapeURL"; import { Engine } from "../scraper/scrapeURL/engines"; configDotenv(); export async function startWebScraperPipeline({ job, - token, + token }: { job: Job & { id: string }; token: string; }) { - return (await runWebScraper({ + return await runWebScraper({ url: job.data.url, mode: job.data.mode, scrapeOptions: { ...job.data.scrapeOptions, - ...(job.data.crawl_id ? ({ - formats: job.data.scrapeOptions.formats.concat(["rawHtml"]), - }): {}), + ...(job.data.crawl_id + ? { + formats: job.data.scrapeOptions.formats.concat(["rawHtml"]) + } + : {}) }, internalOptions: job.data.internalOptions, // onSuccess: (result, mode) => { @@ -42,8 +48,8 @@ export async function startWebScraperPipeline({ team_id: job.data.team_id, bull_job_id: job.id.toString(), priority: job.opts.priority, - is_scrape: job.data.is_scrape ?? false, - })); + is_scrape: job.data.is_scrape ?? false + }); } export async function runWebScraper({ @@ -56,28 +62,40 @@ export async function runWebScraper({ team_id, bull_job_id, priority, - is_scrape=false, + is_scrape = false }: RunWebScraperParams): Promise { let response: ScrapeUrlResponse | undefined = undefined; let engines: EngineResultsTracker = {}; try { - response = await scrapeURL(bull_job_id, url, scrapeOptions, { priority, ...internalOptions }); + response = await scrapeURL(bull_job_id, url, scrapeOptions, { + priority, + ...internalOptions + }); if (!response.success) { if (response.error instanceof Error) { throw response.error; } else { - throw new Error("scrapeURL error: " + (Array.isArray(response.error) ? JSON.stringify(response.error) : typeof response.error === "object" ? JSON.stringify({ ...response.error }) : response.error)); + throw new Error( + "scrapeURL error: " + + (Array.isArray(response.error) + ? JSON.stringify(response.error) + : typeof response.error === "object" + ? JSON.stringify({ ...response.error }) + : response.error) + ); } } - if(is_scrape === false) { + if (is_scrape === false) { let creditsToBeBilled = 1; // Assuming 1 credit per document if (scrapeOptions.extract) { creditsToBeBilled = 5; } - billTeam(team_id, undefined, creditsToBeBilled).catch(error => { - logger.error(`Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`); + billTeam(team_id, undefined, creditsToBeBilled).catch((error) => { + logger.error( + `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}` + ); // Optionally, you could notify an admin or add to a retry queue here }); } @@ -88,42 +106,70 @@ export async function runWebScraper({ engines = response.engines; return response; } catch (error) { - engines = response !== undefined ? response.engines : ((typeof error === "object" && error !== null ? (error as any).results ?? {} : {})); + engines = + response !== undefined + ? response.engines + : typeof error === "object" && error !== null + ? ((error as any).results ?? {}) + : {}; if (response !== undefined) { return { ...response, success: false, - error, - } + error + }; } else { - return { success: false, error, logs: ["no logs -- error coming from runWebScraper"], engines }; + return { + success: false, + error, + logs: ["no logs -- error coming from runWebScraper"], + engines + }; } // onError(error); } finally { - const engineOrder = Object.entries(engines).sort((a, b) => a[1].startedAt - b[1].startedAt).map(x => x[0]) as Engine[]; + const engineOrder = Object.entries(engines) + .sort((a, b) => a[1].startedAt - b[1].startedAt) + .map((x) => x[0]) as Engine[]; for (const engine of engineOrder) { - const result = engines[engine] as Exclude; + const result = engines[engine] as Exclude< + EngineResultsTracker[Engine], + undefined + >; ScrapeEvents.insert(bull_job_id, { type: "scrape", url, method: engine, result: { success: result.state === "success", - response_code: (result.state === "success" ? result.result.statusCode : undefined), - response_size: (result.state === "success" ? result.result.html.length : undefined), - error: (result.state === "error" ? result.error : result.state === "timeout" ? "Timed out" : undefined), - time_taken: result.finishedAt - result.startedAt, - }, + response_code: + result.state === "success" ? result.result.statusCode : undefined, + response_size: + result.state === "success" ? result.result.html.length : undefined, + error: + result.state === "error" + ? result.error + : result.state === "timeout" + ? "Timed out" + : undefined, + time_taken: result.finishedAt - result.startedAt + } }); } } } -const saveJob = async (job: Job, result: any, token: string, mode: string, engines?: EngineResultsTracker) => { +const saveJob = async ( + job: Job, + result: any, + token: string, + mode: string, + engines?: EngineResultsTracker +) => { try { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (useDbAuthentication) { const { data, error } = await supabase_service .from("firecrawl_jobs") @@ -140,12 +186,12 @@ const saveJob = async (job: Job, result: any, token: string, mode: string, engin // } catch (error) { // // I think the job won't exist here anymore // } - // } else { - // try { - // await job.moveToCompleted(result, token, false); - // } catch (error) { - // // I think the job won't exist here anymore - // } + // } else { + // try { + // await job.moveToCompleted(result, token, false); + // } catch (error) { + // // I think the job won't exist here anymore + // } } ScrapeEvents.logJobEvent(job, "completed"); } catch (error) { diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index ac61519a..861ae9fc 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -4,7 +4,7 @@ import { autoscalerController, checkQueuesController, cleanBefore24hCompleteJobsController, - queuesController, + queuesController } from "../controllers/v0/admin/queue"; import { wrap } from "./v1"; import { acucCacheClearController } from "../controllers/v0/admin/acuc-cache-clear"; @@ -26,10 +26,7 @@ adminRouter.get( checkQueuesController ); -adminRouter.get( - `/admin/${process.env.BULL_AUTH_KEY}/queues`, - queuesController -); +adminRouter.get(`/admin/${process.env.BULL_AUTH_KEY}/queues`, queuesController); adminRouter.get( `/admin/${process.env.BULL_AUTH_KEY}/autoscaler`, @@ -38,5 +35,5 @@ adminRouter.get( adminRouter.post( `/admin/${process.env.BULL_AUTH_KEY}/acuc-cache-clear`, - wrap(acucCacheClearController), + wrap(acucCacheClearController) ); diff --git a/apps/api/src/routes/v0.ts b/apps/api/src/routes/v0.ts index 2169c2bd..3a7bda65 100644 --- a/apps/api/src/routes/v0.ts +++ b/apps/api/src/routes/v0.ts @@ -27,4 +27,4 @@ v0Router.post("/v0/search", searchController); // Health/Probe routes v0Router.get("/v0/health/liveness", livenessController); -v0Router.get("/v0/health/readiness", readinessController); \ No newline at end of file +v0Router.get("/v0/health/readiness", readinessController); diff --git a/apps/api/src/routes/v1.ts b/apps/api/src/routes/v1.ts index 048e1efc..206423ba 100644 --- a/apps/api/src/routes/v1.ts +++ b/apps/api/src/routes/v1.ts @@ -4,7 +4,12 @@ import { crawlController } from "../controllers/v1/crawl"; import { scrapeController } from "../../src/controllers/v1/scrape"; import { crawlStatusController } from "../controllers/v1/crawl-status"; import { mapController } from "../controllers/v1/map"; -import { ErrorResponse, RequestWithACUC, RequestWithAuth, RequestWithMaybeAuth } from "../controllers/v1/types"; +import { + ErrorResponse, + RequestWithACUC, + RequestWithAuth, + RequestWithMaybeAuth +} from "../controllers/v1/types"; import { RateLimiterMode } from "../types"; import { authenticateUser } from "../controllers/auth"; import { createIdempotencyKey } from "../services/idempotency/create"; @@ -27,89 +32,114 @@ import { extractController } from "../controllers/v1/extract"; // import { livenessController } from "../controllers/v1/liveness"; // import { readinessController } from "../controllers/v1/readiness"; -function checkCreditsMiddleware(minimum?: number): (req: RequestWithAuth, res: Response, next: NextFunction) => void { - return (req, res, next) => { - (async () => { - if (!minimum && req.body) { - minimum = (req.body as any)?.limit ?? (req.body as any)?.urls?.length ?? 1; - } - const { success, remainingCredits, chunk } = await checkTeamCredits(req.acuc, req.auth.team_id, minimum ?? 1); - if (chunk) { - req.acuc = chunk; - } - if (!success) { - logger.error(`Insufficient credits: ${JSON.stringify({ team_id: req.auth.team_id, minimum, remainingCredits })}`); - if (!res.headersSent) { - return res.status(402).json({ success: false, error: "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing or try changing the request limit to a lower value." }); - } - } - req.account = { remainingCredits }; - next(); - })() - .catch(err => next(err)); - }; -} - -export function authMiddleware(rateLimiterMode: RateLimiterMode): (req: RequestWithMaybeAuth, res: Response, next: NextFunction) => void { - return (req, res, next) => { - (async () => { - const auth = await authenticateUser( - req, - res, - rateLimiterMode, - ); - - if (!auth.success) { - if (!res.headersSent) { - return res.status(auth.status).json({ success: false, error: auth.error }); - } else { - return; - } - } - - const { team_id, plan, chunk } = auth; - - req.auth = { team_id, plan }; - req.acuc = chunk ?? undefined; - if (chunk) { - req.account = { remainingCredits: chunk.remaining_credits }; - } - next(); - })() - .catch(err => next(err)); - } -} - -function idempotencyMiddleware(req: Request, res: Response, next: NextFunction) { +function checkCreditsMiddleware( + minimum?: number +): (req: RequestWithAuth, res: Response, next: NextFunction) => void { + return (req, res, next) => { (async () => { - if (req.headers["x-idempotency-key"]) { - const isIdempotencyValid = await validateIdempotencyKey(req); - if (!isIdempotencyValid) { - if (!res.headersSent) { - return res.status(409).json({ success: false, error: "Idempotency key already used" }); - } - } - createIdempotencyKey(req); + if (!minimum && req.body) { + minimum = + (req.body as any)?.limit ?? (req.body as any)?.urls?.length ?? 1; + } + const { success, remainingCredits, chunk } = await checkTeamCredits( + req.acuc, + req.auth.team_id, + minimum ?? 1 + ); + if (chunk) { + req.acuc = chunk; + } + if (!success) { + logger.error( + `Insufficient credits: ${JSON.stringify({ team_id: req.auth.team_id, minimum, remainingCredits })}` + ); + if (!res.headersSent) { + return res + .status(402) + .json({ + success: false, + error: + "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing or try changing the request limit to a lower value." + }); } - next(); - })() - .catch(err => next(err)); + } + req.account = { remainingCredits }; + next(); + })().catch((err) => next(err)); + }; +} + +export function authMiddleware( + rateLimiterMode: RateLimiterMode +): (req: RequestWithMaybeAuth, res: Response, next: NextFunction) => void { + return (req, res, next) => { + (async () => { + const auth = await authenticateUser(req, res, rateLimiterMode); + + if (!auth.success) { + if (!res.headersSent) { + return res + .status(auth.status) + .json({ success: false, error: auth.error }); + } else { + return; + } + } + + const { team_id, plan, chunk } = auth; + + req.auth = { team_id, plan }; + req.acuc = chunk ?? undefined; + if (chunk) { + req.account = { remainingCredits: chunk.remaining_credits }; + } + next(); + })().catch((err) => next(err)); + }; +} + +function idempotencyMiddleware( + req: Request, + res: Response, + next: NextFunction +) { + (async () => { + if (req.headers["x-idempotency-key"]) { + const isIdempotencyValid = await validateIdempotencyKey(req); + if (!isIdempotencyValid) { + if (!res.headersSent) { + return res + .status(409) + .json({ success: false, error: "Idempotency key already used" }); + } + } + createIdempotencyKey(req); + } + next(); + })().catch((err) => next(err)); } function blocklistMiddleware(req: Request, res: Response, next: NextFunction) { - if (typeof req.body.url === "string" && isUrlBlocked(req.body.url)) { - if (!res.headersSent) { - return res.status(403).json({ success: false, error: "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions." }); - } + if (typeof req.body.url === "string" && isUrlBlocked(req.body.url)) { + if (!res.headersSent) { + return res + .status(403) + .json({ + success: false, + error: + "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions." + }); } - next(); + } + next(); } -export function wrap(controller: (req: Request, res: Response) => Promise): (req: Request, res: Response, next: NextFunction) => any { - return (req, res, next) => { - controller(req, res) - .catch(err => next(err)) - } +export function wrap( + controller: (req: Request, res: Response) => Promise +): (req: Request, res: Response, next: NextFunction) => any { + return (req, res, next) => { + controller(req, res).catch((err) => next(err)); + }; } expressWs(express()); @@ -117,80 +147,71 @@ expressWs(express()); export const v1Router = express.Router(); v1Router.post( - "/scrape", - authMiddleware(RateLimiterMode.Scrape), - checkCreditsMiddleware(1), - blocklistMiddleware, - wrap(scrapeController) + "/scrape", + authMiddleware(RateLimiterMode.Scrape), + checkCreditsMiddleware(1), + blocklistMiddleware, + wrap(scrapeController) ); v1Router.post( - "/crawl", - authMiddleware(RateLimiterMode.Crawl), - checkCreditsMiddleware(), - blocklistMiddleware, - idempotencyMiddleware, - wrap(crawlController) + "/crawl", + authMiddleware(RateLimiterMode.Crawl), + checkCreditsMiddleware(), + blocklistMiddleware, + idempotencyMiddleware, + wrap(crawlController) ); v1Router.post( - "/batch/scrape", - authMiddleware(RateLimiterMode.Crawl), - checkCreditsMiddleware(), - blocklistMiddleware, - idempotencyMiddleware, - wrap(batchScrapeController) + "/batch/scrape", + authMiddleware(RateLimiterMode.Crawl), + checkCreditsMiddleware(), + blocklistMiddleware, + idempotencyMiddleware, + wrap(batchScrapeController) ); v1Router.post( - "/map", - authMiddleware(RateLimiterMode.Map), - checkCreditsMiddleware(1), - blocklistMiddleware, - wrap(mapController) + "/map", + authMiddleware(RateLimiterMode.Map), + checkCreditsMiddleware(1), + blocklistMiddleware, + wrap(mapController) ); v1Router.get( - "/crawl/:jobId", - authMiddleware(RateLimiterMode.CrawlStatus), - wrap(crawlStatusController) + "/crawl/:jobId", + authMiddleware(RateLimiterMode.CrawlStatus), + wrap(crawlStatusController) ); v1Router.get( - "/batch/scrape/:jobId", - authMiddleware(RateLimiterMode.CrawlStatus), - // Yes, it uses the same controller as the normal crawl status controller - wrap((req:any, res):any => crawlStatusController(req, res, true)) + "/batch/scrape/:jobId", + authMiddleware(RateLimiterMode.CrawlStatus), + // Yes, it uses the same controller as the normal crawl status controller + wrap((req: any, res): any => crawlStatusController(req, res, true)) ); +v1Router.get("/scrape/:jobId", wrap(scrapeStatusController)); + v1Router.get( - "/scrape/:jobId", - wrap(scrapeStatusController) + "/concurrency-check", + authMiddleware(RateLimiterMode.CrawlStatus), + wrap(concurrencyCheckController) ); -v1Router.get( - "/concurrency-check", - authMiddleware(RateLimiterMode.CrawlStatus), - wrap(concurrencyCheckController) -); - -v1Router.ws( - "/crawl/:jobId", - crawlStatusWSController -); +v1Router.ws("/crawl/:jobId", crawlStatusWSController); v1Router.post( - "/extract", - authMiddleware(RateLimiterMode.Scrape), - checkCreditsMiddleware(1), - wrap(extractController) + "/extract", + authMiddleware(RateLimiterMode.Scrape), + checkCreditsMiddleware(1), + wrap(extractController) ); - - // v1Router.post("/crawlWebsitePreview", crawlPreviewController); - v1Router.delete( "/crawl/:jobId", authMiddleware(RateLimiterMode.CrawlStatus), @@ -207,4 +228,3 @@ v1Router.delete( // Health/Probe routes // v1Router.get("/health/liveness", livenessController); // v1Router.get("/health/readiness", readinessController); - diff --git a/apps/api/src/run-req.ts b/apps/api/src/run-req.ts index 6d29916d..61ee61bd 100644 --- a/apps/api/src/run-req.ts +++ b/apps/api/src/run-req.ts @@ -18,19 +18,19 @@ async function sendCrawl(result: Result): Promise { { url: url, crawlerOptions: { - limit: 75, + limit: 75 }, pageOptions: { includeHtml: true, replaceAllPathsWithAbsolutePaths: true, - waitFor: 1000, - }, + waitFor: 1000 + } }, { headers: { "Content-Type": "application/json", - Authorization: `Bearer `, - }, + Authorization: `Bearer ` + } } ); result.idempotency_key = idempotencyKey; @@ -51,8 +51,8 @@ async function getContent(result: Result): Promise { { headers: { "Content-Type": "application/json", - Authorization: `Bearer `, - }, + Authorization: `Bearer ` + } } ); if (response.data.status === "completed") { @@ -95,9 +95,9 @@ async function processResults(results: Result[]): Promise { // Save the result to the file try { // Save job id along with the start_url - const resultWithJobId = results.map(r => ({ + const resultWithJobId = results.map((r) => ({ start_url: r.start_url, - job_id: r.job_id, + job_id: r.job_id })); await fs.writeFile( "results_with_job_id_4000_6000.json", diff --git a/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts b/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts index eba0ddb4..da2b7d61 100644 --- a/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts +++ b/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts @@ -1,27 +1,29 @@ // crawler.test.ts -import { WebCrawler } from '../crawler'; -import axios from 'axios'; -import robotsParser from 'robots-parser'; +import { WebCrawler } from "../crawler"; +import axios from "axios"; +import robotsParser from "robots-parser"; -jest.mock('axios'); -jest.mock('robots-parser'); +jest.mock("axios"); +jest.mock("robots-parser"); -describe('WebCrawler', () => { +describe("WebCrawler", () => { let crawler: WebCrawler; const mockAxios = axios as jest.Mocked; - const mockRobotsParser = robotsParser as jest.MockedFunction; + const mockRobotsParser = robotsParser as jest.MockedFunction< + typeof robotsParser + >; let maxCrawledDepth: number; beforeEach(() => { // Setup default mocks mockAxios.get.mockImplementation((url) => { - if (url.includes('robots.txt')) { - return Promise.resolve({ data: 'User-agent: *\nAllow: /' }); - } else if (url.includes('sitemap.xml')) { - return Promise.resolve({ data: 'sitemap content' }); // You would normally parse this to URLs + if (url.includes("robots.txt")) { + return Promise.resolve({ data: "User-agent: *\nAllow: /" }); + } else if (url.includes("sitemap.xml")) { + return Promise.resolve({ data: "sitemap content" }); // You would normally parse this to URLs } - return Promise.resolve({ data: '' }); + return Promise.resolve({ data: "" }); }); mockRobotsParser.mockReturnValue({ @@ -30,42 +32,45 @@ describe('WebCrawler', () => { getMatchingLineNumber: jest.fn().mockReturnValue(0), getCrawlDelay: jest.fn().mockReturnValue(0), getSitemaps: jest.fn().mockReturnValue([]), - getPreferredHost: jest.fn().mockReturnValue('example.com') + getPreferredHost: jest.fn().mockReturnValue("example.com") }); }); - it('should respect the limit parameter by not returning more links than specified', async () => { - const initialUrl = 'http://example.com'; - const limit = 2; // Set a limit for the number of links + it("should respect the limit parameter by not returning more links than specified", async () => { + const initialUrl = "http://example.com"; + const limit = 2; // Set a limit for the number of links crawler = new WebCrawler({ jobId: "TEST", initialUrl: initialUrl, includes: [], excludes: [], - limit: limit, // Apply the limit + limit: limit, // Apply the limit maxCrawledDepth: 10 }); // Mock sitemap fetching function to return more links than the limit - crawler['tryFetchSitemapLinks'] = jest.fn().mockResolvedValue([ - initialUrl, - initialUrl + '/page1', - initialUrl + '/page2', - initialUrl + '/page3' - ]); + crawler["tryFetchSitemapLinks"] = jest + .fn() + .mockResolvedValue([ + initialUrl, + initialUrl + "/page1", + initialUrl + "/page2", + initialUrl + "/page3" + ]); - const filteredLinks = crawler['filterLinks']( - [initialUrl, initialUrl + '/page1', initialUrl + '/page2', initialUrl + '/page3'], + const filteredLinks = crawler["filterLinks"]( + [ + initialUrl, + initialUrl + "/page1", + initialUrl + "/page2", + initialUrl + "/page3" + ], limit, 10 ); - expect(filteredLinks.length).toBe(limit); // Check if the number of results respects the limit - expect(filteredLinks).toEqual([ - initialUrl, - initialUrl + '/page1' - ]); + expect(filteredLinks.length).toBe(limit); // Check if the number of results respects the limit + expect(filteredLinks).toEqual([initialUrl, initialUrl + "/page1"]); }); }); - diff --git a/apps/api/src/scraper/WebScraper/__tests__/dns.test.ts b/apps/api/src/scraper/WebScraper/__tests__/dns.test.ts index 968ed121..662a7376 100644 --- a/apps/api/src/scraper/WebScraper/__tests__/dns.test.ts +++ b/apps/api/src/scraper/WebScraper/__tests__/dns.test.ts @@ -1,5 +1,5 @@ -import CacheableLookup from 'cacheable-lookup'; -import https from 'node:https'; +import CacheableLookup from "cacheable-lookup"; +import https from "node:https"; import axios from "axios"; describe("DNS", () => { diff --git a/apps/api/src/scraper/WebScraper/crawler.ts b/apps/api/src/scraper/WebScraper/crawler.ts index cac03a68..be3cdf72 100644 --- a/apps/api/src/scraper/WebScraper/crawler.ts +++ b/apps/api/src/scraper/WebScraper/crawler.ts @@ -40,7 +40,7 @@ export class WebCrawler { allowBackwardCrawling = false, allowExternalContentLinks = false, allowSubdomains = false, - ignoreRobotsTxt = false, + ignoreRobotsTxt = false }: { jobId: string; initialUrl: string; @@ -75,9 +75,14 @@ export class WebCrawler { this.logger = _logger.child({ crawlId: this.jobId, module: "WebCrawler" }); } - public filterLinks(sitemapLinks: string[], limit: number, maxDepth: number, fromMap: boolean = false): string[] { + public filterLinks( + sitemapLinks: string[], + limit: number, + maxDepth: number, + fromMap: boolean = false + ): string[] { // If the initial URL is a sitemap.xml, skip filtering - if (this.initialUrl.endsWith('sitemap.xml') && fromMap) { + if (this.initialUrl.endsWith("sitemap.xml") && fromMap) { return sitemapLinks.slice(0, limit); } @@ -87,14 +92,17 @@ export class WebCrawler { try { url = new URL(link.trim(), this.baseUrl); } catch (error) { - this.logger.debug(`Error processing link: ${link}`, { link, error, method: "filterLinks" }); + this.logger.debug(`Error processing link: ${link}`, { + link, + error, + method: "filterLinks" + }); return false; } const path = url.pathname; - + const depth = getURLDepth(url.toString()); - // Check if the link exceeds the maximum depth allowed if (depth > maxDepth) { return false; @@ -113,9 +121,11 @@ export class WebCrawler { // Check if the link matches the include patterns, if any are specified if (this.includes.length > 0 && this.includes[0] !== "") { - if (!this.includes.some((includePattern) => - new RegExp(includePattern).test(path) - )) { + if ( + !this.includes.some((includePattern) => + new RegExp(includePattern).test(path) + ) + ) { return false; } } @@ -128,8 +138,11 @@ export class WebCrawler { } catch (_) { return false; } - const initialHostname = normalizedInitialUrl.hostname.replace(/^www\./, ''); - const linkHostname = normalizedLink.hostname.replace(/^www\./, ''); + const initialHostname = normalizedInitialUrl.hostname.replace( + /^www\./, + "" + ); + const linkHostname = normalizedLink.hostname.replace(/^www\./, ""); // Ensure the protocol and hostname match, and the path starts with the initial URL's path // commented to able to handling external link on allowExternalContentLinks @@ -138,15 +151,22 @@ export class WebCrawler { // } if (!this.allowBackwardCrawling) { - if (!normalizedLink.pathname.startsWith(normalizedInitialUrl.pathname)) { + if ( + !normalizedLink.pathname.startsWith(normalizedInitialUrl.pathname) + ) { return false; } } - const isAllowed = this.ignoreRobotsTxt ? true : (this.robots.isAllowed(link, "FireCrawlAgent") ?? true); + const isAllowed = this.ignoreRobotsTxt + ? true + : (this.robots.isAllowed(link, "FireCrawlAgent") ?? true); // Check if the link is disallowed by robots.txt if (!isAllowed) { - this.logger.debug(`Link disallowed by robots.txt: ${link}`, { method: "filterLinks", link }); + this.logger.debug(`Link disallowed by robots.txt: ${link}`, { + method: "filterLinks", + link + }); return false; } @@ -161,12 +181,15 @@ export class WebCrawler { public async getRobotsTxt(skipTlsVerification = false): Promise { let extraArgs = {}; - if(skipTlsVerification) { + if (skipTlsVerification) { extraArgs["httpsAgent"] = new https.Agent({ rejectUnauthorized: false }); } - const response = await axios.get(this.robotsTxtUrl, { timeout: axiosTimeout, ...extraArgs }); + const response = await axios.get(this.robotsTxtUrl, { + timeout: axiosTimeout, + ...extraArgs + }); return response.data; } @@ -174,15 +197,25 @@ export class WebCrawler { this.robots = robotsParser(this.robotsTxtUrl, txt); } - public async tryGetSitemap(fromMap: boolean = false, onlySitemap: boolean = false): Promise<{ url: string; html: string; }[] | null> { - this.logger.debug(`Fetching sitemap links from ${this.initialUrl}`, { method: "tryGetSitemap" }); + public async tryGetSitemap( + fromMap: boolean = false, + onlySitemap: boolean = false + ): Promise<{ url: string; html: string }[] | null> { + this.logger.debug(`Fetching sitemap links from ${this.initialUrl}`, { + method: "tryGetSitemap" + }); const sitemapLinks = await this.tryFetchSitemapLinks(this.initialUrl); - if(fromMap && onlySitemap) { - return sitemapLinks.map(link => ({ url: link, html: "" })); + if (fromMap && onlySitemap) { + return sitemapLinks.map((link) => ({ url: link, html: "" })); } if (sitemapLinks.length > 0) { - let filteredLinks = this.filterLinks(sitemapLinks, this.limit, this.maxCrawledDepth, fromMap); - return filteredLinks.map(link => ({ url: link, html: "" })); + let filteredLinks = this.filterLinks( + sitemapLinks, + this.limit, + this.maxCrawledDepth, + fromMap + ); + return filteredLinks.map((link) => ({ url: link, html: "" })); } return null; } @@ -204,15 +237,18 @@ export class WebCrawler { } const path = urlObj.pathname; - if (this.isInternalLink(fullUrl)) { // INTERNAL LINKS - if (this.isInternalLink(fullUrl) && + if (this.isInternalLink(fullUrl)) { + // INTERNAL LINKS + if ( + this.isInternalLink(fullUrl) && this.noSections(fullUrl) && !this.matchesExcludes(path) && this.isRobotsAllowed(fullUrl, this.ignoreRobotsTxt) ) { return fullUrl; } - } else { // EXTERNAL LINKS + } else { + // EXTERNAL LINKS if ( this.isInternalLink(url) && this.allowExternalContentLinks && @@ -224,7 +260,11 @@ export class WebCrawler { } } - if (this.allowSubdomains && !this.isSocialMediaOrEmail(fullUrl) && this.isSubdomain(fullUrl)) { + if ( + this.allowSubdomains && + !this.isSocialMediaOrEmail(fullUrl) && + this.isSubdomain(fullUrl) + ) { return fullUrl; } @@ -261,14 +301,20 @@ export class WebCrawler { return links; } - private isRobotsAllowed(url: string, ignoreRobotsTxt: boolean = false): boolean { - return (ignoreRobotsTxt ? true : (this.robots ? (this.robots.isAllowed(url, "FireCrawlAgent") ?? true) : true)) + private isRobotsAllowed( + url: string, + ignoreRobotsTxt: boolean = false + ): boolean { + return ignoreRobotsTxt + ? true + : this.robots + ? (this.robots.isAllowed(url, "FireCrawlAgent") ?? true) + : true; } private matchesExcludes(url: string, onlyDomains: boolean = false): boolean { return this.excludes.some((pattern) => { - if (onlyDomains) - return this.matchesExcludesExternalDomains(url); + if (onlyDomains) return this.matchesExcludesExternalDomains(url); return this.excludes.some((pattern) => new RegExp(pattern).test(url)); }); @@ -282,11 +328,14 @@ export class WebCrawler { const pathname = urlObj.pathname; for (let domain of this.excludes) { - let domainObj = new URL('http://' + domain.replace(/^https?:\/\//, '')); + let domainObj = new URL("http://" + domain.replace(/^https?:\/\//, "")); let domainHostname = domainObj.hostname; let domainPathname = domainObj.pathname; - if (hostname === domainHostname || hostname.endsWith(`.${domainHostname}`)) { + if ( + hostname === domainHostname || + hostname.endsWith(`.${domainHostname}`) + ) { if (pathname.startsWith(domainPathname)) { return true; } @@ -298,8 +347,13 @@ export class WebCrawler { } } - private isExternalMainPage(url:string):boolean { - return !Boolean(url.split("/").slice(3).filter(subArray => subArray.length > 0).length) + private isExternalMainPage(url: string): boolean { + return !Boolean( + url + .split("/") + .slice(3) + .filter((subArray) => subArray.length > 0).length + ); } private noSections(link: string): boolean { @@ -308,14 +362,19 @@ export class WebCrawler { private isInternalLink(link: string): boolean { const urlObj = new URL(link, this.baseUrl); - const baseDomain = this.baseUrl.replace(/^https?:\/\//, "").replace(/^www\./, "").trim(); + const baseDomain = this.baseUrl + .replace(/^https?:\/\//, "") + .replace(/^www\./, "") + .trim(); const linkDomain = urlObj.hostname.replace(/^www\./, "").trim(); - + return linkDomain === baseDomain; } private isSubdomain(link: string): boolean { - return new URL(link, this.baseUrl).hostname.endsWith("." + new URL(this.baseUrl).hostname.split(".").slice(-2).join(".")); + return new URL(link, this.baseUrl).hostname.endsWith( + "." + new URL(this.baseUrl).hostname.split(".").slice(-2).join(".") + ); } public isFile(url: string): boolean { @@ -329,7 +388,7 @@ export class WebCrawler { ".ico", ".svg", ".tiff", - // ".pdf", + // ".pdf", ".zip", ".exe", ".dmg", @@ -350,10 +409,13 @@ export class WebCrawler { ]; try { - const urlWithoutQuery = url.split('?')[0].toLowerCase(); + const urlWithoutQuery = url.split("?")[0].toLowerCase(); return fileExtensions.some((ext) => urlWithoutQuery.endsWith(ext)); } catch (error) { - this.logger.error(`Error processing URL in isFile`, { method: "isFile", error }); + this.logger.error(`Error processing URL in isFile`, { + method: "isFile", + error + }); return false; } } @@ -369,7 +431,7 @@ export class WebCrawler { "github.com", "calendly.com", "discord.gg", - "discord.com", + "discord.com" ]; return socialMediaOrEmail.some((ext) => url.includes(ext)); } @@ -383,10 +445,7 @@ export class WebCrawler { return url; }; - - const sitemapUrl = url.endsWith(".xml") - ? url - : `${url}/sitemap.xml`; + const sitemapUrl = url.endsWith(".xml") ? url : `${url}/sitemap.xml`; let sitemapLinks: string[] = []; @@ -395,12 +454,18 @@ export class WebCrawler { if (response.status === 200) { sitemapLinks = await getLinksFromSitemap({ sitemapUrl }, this.logger); } - } catch (error) { - this.logger.debug(`Failed to fetch sitemap with axios from ${sitemapUrl}`, { method: "tryFetchSitemapLinks", sitemapUrl, error }); + } catch (error) { + this.logger.debug( + `Failed to fetch sitemap with axios from ${sitemapUrl}`, + { method: "tryFetchSitemapLinks", sitemapUrl, error } + ); if (error instanceof AxiosError && error.response?.status === 404) { // ignore 404 } else { - const response = await getLinksFromSitemap({ sitemapUrl, mode: 'fire-engine' }, this.logger); + const response = await getLinksFromSitemap( + { sitemapUrl, mode: "fire-engine" }, + this.logger + ); if (response) { sitemapLinks = response; } @@ -410,24 +475,41 @@ export class WebCrawler { if (sitemapLinks.length === 0) { const baseUrlSitemap = `${this.baseUrl}/sitemap.xml`; try { - const response = await axios.get(baseUrlSitemap, { timeout: axiosTimeout }); + const response = await axios.get(baseUrlSitemap, { + timeout: axiosTimeout + }); if (response.status === 200) { - sitemapLinks = await getLinksFromSitemap({ sitemapUrl: baseUrlSitemap, mode: 'fire-engine' }, this.logger); + sitemapLinks = await getLinksFromSitemap( + { sitemapUrl: baseUrlSitemap, mode: "fire-engine" }, + this.logger + ); } } catch (error) { - this.logger.debug(`Failed to fetch sitemap from ${baseUrlSitemap}`, { method: "tryFetchSitemapLinks", sitemapUrl: baseUrlSitemap, error }); + this.logger.debug(`Failed to fetch sitemap from ${baseUrlSitemap}`, { + method: "tryFetchSitemapLinks", + sitemapUrl: baseUrlSitemap, + error + }); if (error instanceof AxiosError && error.response?.status === 404) { // ignore 404 } else { - sitemapLinks = await getLinksFromSitemap({ sitemapUrl: baseUrlSitemap, mode: 'fire-engine' }, this.logger); + sitemapLinks = await getLinksFromSitemap( + { sitemapUrl: baseUrlSitemap, mode: "fire-engine" }, + this.logger + ); } } } const normalizedUrl = normalizeUrl(url); - const normalizedSitemapLinks = sitemapLinks.map(link => normalizeUrl(link)); + const normalizedSitemapLinks = sitemapLinks.map((link) => + normalizeUrl(link) + ); // has to be greater than 0 to avoid adding the initial URL to the sitemap links, and preventing crawler to crawl - if (!normalizedSitemapLinks.includes(normalizedUrl) && sitemapLinks.length > 0) { + if ( + !normalizedSitemapLinks.includes(normalizedUrl) && + sitemapLinks.length > 0 + ) { sitemapLinks.push(url); } return sitemapLinks; diff --git a/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts b/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts index 48aa2ffd..ba77b78b 100644 --- a/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts +++ b/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts @@ -3,9 +3,17 @@ import { logger } from "../../../lib/logger"; export async function handleCustomScraping( text: string, url: string -): Promise<{ scraper: string; url: string; waitAfterLoad?: number, pageOptions?: { scrollXPaths?: string[] } } | null> { +): Promise<{ + scraper: string; + url: string; + waitAfterLoad?: number; + pageOptions?: { scrollXPaths?: string[] }; +} | null> { // Check for Readme Docs special case - if (text.includes(' { try { let content: string = ""; try { - if (mode === 'axios' || process.env.FIRE_ENGINE_BETA_URL === '') { + if (mode === "axios" || process.env.FIRE_ENGINE_BETA_URL === "") { const response = await axios.get(sitemapUrl, { timeout: axiosTimeout }); content = response.data; - } else if (mode === 'fire-engine') { - const response = await scrapeURL("sitemap", sitemapUrl, scrapeOptions.parse({ formats: ["rawHtml"] }), { forceEngine: "fire-engine;tlsclient", v0DisableJsDom: true }); + } else if (mode === "fire-engine") { + const response = await scrapeURL( + "sitemap", + sitemapUrl, + scrapeOptions.parse({ formats: ["rawHtml"] }), + { forceEngine: "fire-engine;tlsclient", v0DisableJsDom: true } + ); if (!response.success) { throw response.error; } content = response.document.rawHtml!; } } catch (error) { - logger.error(`Request failed for ${sitemapUrl}`, { method: "getLinksFromSitemap", mode, sitemapUrl, error }); + logger.error(`Request failed for ${sitemapUrl}`, { + method: "getLinksFromSitemap", + mode, + sitemapUrl, + error + }); return allUrls; } @@ -42,26 +52,46 @@ export async function getLinksFromSitemap( if (root && root.sitemap) { const sitemapPromises = root.sitemap - .filter(sitemap => sitemap.loc && sitemap.loc.length > 0) - .map(sitemap => getLinksFromSitemap({ sitemapUrl: sitemap.loc[0], allUrls, mode }, logger)); + .filter((sitemap) => sitemap.loc && sitemap.loc.length > 0) + .map((sitemap) => + getLinksFromSitemap( + { sitemapUrl: sitemap.loc[0], allUrls, mode }, + logger + ) + ); await Promise.all(sitemapPromises); } else if (root && root.url) { const validUrls = root.url - .filter(url => url.loc && url.loc.length > 0 && !WebCrawler.prototype.isFile(url.loc[0])) - .map(url => url.loc[0]); + .filter( + (url) => + url.loc && + url.loc.length > 0 && + !WebCrawler.prototype.isFile(url.loc[0]) + ) + .map((url) => url.loc[0]); allUrls.push(...validUrls); } } catch (error) { - logger.debug(`Error processing sitemapUrl: ${sitemapUrl}`, { method: "getLinksFromSitemap", mode, sitemapUrl, error }); + logger.debug(`Error processing sitemapUrl: ${sitemapUrl}`, { + method: "getLinksFromSitemap", + mode, + sitemapUrl, + error + }); } return allUrls; } -export const fetchSitemapData = async (url: string, timeout?: number): Promise => { +export const fetchSitemapData = async ( + url: string, + timeout?: number +): Promise => { const sitemapUrl = url.endsWith("/sitemap.xml") ? url : `${url}/sitemap.xml`; try { - const response = await axios.get(sitemapUrl, { timeout: timeout || axiosTimeout }); + const response = await axios.get(sitemapUrl, { + timeout: timeout || axiosTimeout + }); if (response.status === 200) { const xml = response.data; const parsedXml = await parseStringPromise(xml); @@ -71,8 +101,10 @@ export const fetchSitemapData = async (url: string, timeout?: number): Promise { - describe('isUrlBlocked', () => { +describe("Blocklist Functionality", () => { + describe("isUrlBlocked", () => { test.each([ - 'https://facebook.com/fake-test', - 'https://x.com/user-profile', - 'https://twitter.com/home', - 'https://instagram.com/explore', - 'https://linkedin.com/in/johndoe', - 'https://snapchat.com/add/johndoe', - 'https://tiktok.com/@johndoe', - 'https://reddit.com/r/funny', - 'https://tumblr.com/dashboard', - 'https://flickr.com/photos/johndoe', - 'https://whatsapp.com/download', - 'https://wechat.com/features', - 'https://telegram.org/apps' - ])('should return true for blocklisted URL %s', (url) => { + "https://facebook.com/fake-test", + "https://x.com/user-profile", + "https://twitter.com/home", + "https://instagram.com/explore", + "https://linkedin.com/in/johndoe", + "https://snapchat.com/add/johndoe", + "https://tiktok.com/@johndoe", + "https://reddit.com/r/funny", + "https://tumblr.com/dashboard", + "https://flickr.com/photos/johndoe", + "https://whatsapp.com/download", + "https://wechat.com/features", + "https://telegram.org/apps" + ])("should return true for blocklisted URL %s", (url) => { expect(isUrlBlocked(url)).toBe(true); }); test.each([ - 'https://facebook.com/policy', - 'https://twitter.com/tos', - 'https://instagram.com/about/legal/terms', - 'https://linkedin.com/legal/privacy-policy', - 'https://pinterest.com/about/privacy', - 'https://snapchat.com/legal/terms', - 'https://tiktok.com/legal/privacy-policy', - 'https://reddit.com/policies', - 'https://tumblr.com/policy/en/privacy', - 'https://flickr.com/help/terms', - 'https://whatsapp.com/legal', - 'https://wechat.com/en/privacy-policy', - 'https://telegram.org/tos' - ])('should return false for allowed URLs with keywords %s', (url) => { + "https://facebook.com/policy", + "https://twitter.com/tos", + "https://instagram.com/about/legal/terms", + "https://linkedin.com/legal/privacy-policy", + "https://pinterest.com/about/privacy", + "https://snapchat.com/legal/terms", + "https://tiktok.com/legal/privacy-policy", + "https://reddit.com/policies", + "https://tumblr.com/policy/en/privacy", + "https://flickr.com/help/terms", + "https://whatsapp.com/legal", + "https://wechat.com/en/privacy-policy", + "https://telegram.org/tos" + ])("should return false for allowed URLs with keywords %s", (url) => { expect(isUrlBlocked(url)).toBe(false); }); - test('should return false for non-blocklisted domain', () => { - const url = 'https://example.com'; + test("should return false for non-blocklisted domain", () => { + const url = "https://example.com"; expect(isUrlBlocked(url)).toBe(false); }); - test('should handle invalid URLs gracefully', () => { - const url = 'htp://invalid-url'; + test("should handle invalid URLs gracefully", () => { + const url = "htp://invalid-url"; expect(isUrlBlocked(url)).toBe(false); }); }); test.each([ - 'https://subdomain.facebook.com', - 'https://facebook.com.someotherdomain.com', - 'https://www.facebook.com/profile', - 'https://api.twitter.com/info', - 'https://instagram.com/accounts/login' - ])('should return true for URLs with blocklisted domains in subdomains or paths %s', (url) => { + "https://subdomain.facebook.com", + "https://facebook.com.someotherdomain.com", + "https://www.facebook.com/profile", + "https://api.twitter.com/info", + "https://instagram.com/accounts/login" + ])( + "should return true for URLs with blocklisted domains in subdomains or paths %s", + (url) => { + expect(isUrlBlocked(url)).toBe(true); + } + ); + + test.each([ + "https://example.com/facebook.com", + "https://example.com/redirect?url=https://twitter.com", + "https://facebook.com.policy.example.com" + ])( + "should return false for URLs where blocklisted domain is part of another domain or path %s", + (url) => { + expect(isUrlBlocked(url)).toBe(false); + } + ); + + test.each(["https://FACEBOOK.com", "https://INSTAGRAM.com/@something"])( + "should handle case variations %s", + (url) => { + expect(isUrlBlocked(url)).toBe(true); + } + ); + + test.each([ + "https://facebook.com?redirect=https://example.com", + "https://twitter.com?query=something" + ])("should handle query parameters %s", (url) => { expect(isUrlBlocked(url)).toBe(true); }); - test.each([ - 'https://example.com/facebook.com', - 'https://example.com/redirect?url=https://twitter.com', - 'https://facebook.com.policy.example.com' - ])('should return false for URLs where blocklisted domain is part of another domain or path %s', (url) => { + test("should handle internationalized domain names", () => { + const url = "https://xn--d1acpjx3f.xn--p1ai"; expect(isUrlBlocked(url)).toBe(false); }); - - test.each([ - 'https://FACEBOOK.com', - 'https://INSTAGRAM.com/@something' - ])('should handle case variations %s', (url) => { - expect(isUrlBlocked(url)).toBe(true); - }); - - test.each([ - 'https://facebook.com?redirect=https://example.com', - 'https://twitter.com?query=something' - ])('should handle query parameters %s', (url) => { - expect(isUrlBlocked(url)).toBe(true); - }); - - test('should handle internationalized domain names', () => { - const url = 'https://xn--d1acpjx3f.xn--p1ai'; - expect(isUrlBlocked(url)).toBe(false); - }); -}); \ No newline at end of file +}); diff --git a/apps/api/src/scraper/WebScraper/utils/__tests__/maxDepthUtils.test.ts b/apps/api/src/scraper/WebScraper/utils/__tests__/maxDepthUtils.test.ts index 863a6893..4cfa2686 100644 --- a/apps/api/src/scraper/WebScraper/utils/__tests__/maxDepthUtils.test.ts +++ b/apps/api/src/scraper/WebScraper/utils/__tests__/maxDepthUtils.test.ts @@ -1,47 +1,42 @@ -import { getURLDepth, getAdjustedMaxDepth } from '../maxDepthUtils'; +import { getURLDepth, getAdjustedMaxDepth } from "../maxDepthUtils"; -describe('Testing getURLDepth and getAdjustedMaxDepth', () => { - it('should return 0 for root - mendable.ai', () => { - const enteredURL = "https://www.mendable.ai/" +describe("Testing getURLDepth and getAdjustedMaxDepth", () => { + it("should return 0 for root - mendable.ai", () => { + const enteredURL = "https://www.mendable.ai/"; expect(getURLDepth(enteredURL)).toBe(0); }); - it('should return 0 for root - scrapethissite.com', () => { - const enteredURL = "https://scrapethissite.com/" + it("should return 0 for root - scrapethissite.com", () => { + const enteredURL = "https://scrapethissite.com/"; expect(getURLDepth(enteredURL)).toBe(0); }); - it('should return 1 for scrapethissite.com/pages', () => { - const enteredURL = "https://scrapethissite.com/pages" + it("should return 1 for scrapethissite.com/pages", () => { + const enteredURL = "https://scrapethissite.com/pages"; expect(getURLDepth(enteredURL)).toBe(1); }); - it('should return 2 for scrapethissite.com/pages/articles', () => { - const enteredURL = "https://scrapethissite.com/pages/articles" + it("should return 2 for scrapethissite.com/pages/articles", () => { + const enteredURL = "https://scrapethissite.com/pages/articles"; expect(getURLDepth(enteredURL)).toBe(2); - }); - it('Adjusted maxDepth should return 1 for scrapethissite.com and max depth param of 1', () => { - const enteredURL = "https://scrapethissite.com" + it("Adjusted maxDepth should return 1 for scrapethissite.com and max depth param of 1", () => { + const enteredURL = "https://scrapethissite.com"; expect(getAdjustedMaxDepth(enteredURL, 1)).toBe(1); - }); - it('Adjusted maxDepth should return 0 for scrapethissite.com and max depth param of 0', () => { - const enteredURL = "https://scrapethissite.com" - expect(getAdjustedMaxDepth(enteredURL, 0)).toBe(0); - - }); - - it('Adjusted maxDepth should return 0 for mendable.ai and max depth param of 0', () => { - const enteredURL = "https://mendable.ai" + it("Adjusted maxDepth should return 0 for scrapethissite.com and max depth param of 0", () => { + const enteredURL = "https://scrapethissite.com"; expect(getAdjustedMaxDepth(enteredURL, 0)).toBe(0); }); - it('Adjusted maxDepth should return 4 for scrapethissite.com/pages/articles and max depth param of 2', () => { - const enteredURL = "https://scrapethissite.com/pages/articles" + it("Adjusted maxDepth should return 0 for mendable.ai and max depth param of 0", () => { + const enteredURL = "https://mendable.ai"; + expect(getAdjustedMaxDepth(enteredURL, 0)).toBe(0); + }); + + it("Adjusted maxDepth should return 4 for scrapethissite.com/pages/articles and max depth param of 2", () => { + const enteredURL = "https://scrapethissite.com/pages/articles"; expect(getAdjustedMaxDepth(enteredURL, 2)).toBe(4); }); - - }); diff --git a/apps/api/src/scraper/WebScraper/utils/blocklist.ts b/apps/api/src/scraper/WebScraper/utils/blocklist.ts index dea4c614..e60943e6 100644 --- a/apps/api/src/scraper/WebScraper/utils/blocklist.ts +++ b/apps/api/src/scraper/WebScraper/utils/blocklist.ts @@ -1,68 +1,75 @@ import { logger } from "../../../lib/logger"; const socialMediaBlocklist = [ - 'facebook.com', - 'x.com', - 'twitter.com', - 'instagram.com', - 'linkedin.com', - 'snapchat.com', - 'tiktok.com', - 'reddit.com', - 'tumblr.com', - 'flickr.com', - 'whatsapp.com', - 'wechat.com', - 'telegram.org', - 'researchhub.com', - 'youtube.com', - 'corterix.com', - 'southwest.com', - 'ryanair.com' + "facebook.com", + "x.com", + "twitter.com", + "instagram.com", + "linkedin.com", + "snapchat.com", + "tiktok.com", + "reddit.com", + "tumblr.com", + "flickr.com", + "whatsapp.com", + "wechat.com", + "telegram.org", + "researchhub.com", + "youtube.com", + "corterix.com", + "southwest.com", + "ryanair.com" ]; const allowedKeywords = [ - 'pulse', - 'privacy', - 'terms', - 'policy', - 'user-agreement', - 'legal', - 'help', - 'policies', - 'support', - 'contact', - 'about', - 'careers', - 'blog', - 'press', - 'conditions', - 'tos', - '://library.tiktok.com', - '://ads.tiktok.com', - '://tiktok.com/business', - '://developers.facebook.com' + "pulse", + "privacy", + "terms", + "policy", + "user-agreement", + "legal", + "help", + "policies", + "support", + "contact", + "about", + "careers", + "blog", + "press", + "conditions", + "tos", + "://library.tiktok.com", + "://ads.tiktok.com", + "://tiktok.com/business", + "://developers.facebook.com" ]; export function isUrlBlocked(url: string): boolean { const lowerCaseUrl = url.toLowerCase(); // Check if the URL contains any allowed keywords as whole words - if (allowedKeywords.some(keyword => new RegExp(`\\b${keyword}\\b`, 'i').test(lowerCaseUrl))) { + if ( + allowedKeywords.some((keyword) => + new RegExp(`\\b${keyword}\\b`, "i").test(lowerCaseUrl) + ) + ) { return false; } try { - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url; + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; } - + const urlObj = new URL(url); const hostname = urlObj.hostname.toLowerCase(); // Check if the URL matches any domain in the blocklist - const isBlocked = socialMediaBlocklist.some(domain => { - const domainPattern = new RegExp(`(^|\\.)${domain.replace('.', '\\.')}(\\.|$)`, 'i'); + const isBlocked = socialMediaBlocklist.some((domain) => { + const domainPattern = new RegExp( + `(^|\\.)${domain.replace(".", "\\.")}(\\.|$)`, + "i" + ); return domainPattern.test(hostname); }); diff --git a/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts b/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts index bcacc210..3db7c5c1 100644 --- a/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts +++ b/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts @@ -1,12 +1,15 @@ - - -export function getAdjustedMaxDepth(url: string, maxCrawlDepth: number): number { +export function getAdjustedMaxDepth( + url: string, + maxCrawlDepth: number +): number { const baseURLDepth = getURLDepth(url); const adjustedMaxDepth = maxCrawlDepth + baseURLDepth; return adjustedMaxDepth; } export function getURLDepth(url: string): number { - const pathSplits = new URL(url).pathname.split('/').filter(x => x !== "" && x !== "index.php" && x !== "index.html"); + const pathSplits = new URL(url).pathname + .split("/") + .filter((x) => x !== "" && x !== "index.php" && x !== "index.html"); return pathSplits.length; } diff --git a/apps/api/src/scraper/WebScraper/utils/removeBase64Images.ts b/apps/api/src/scraper/WebScraper/utils/removeBase64Images.ts index 2845589c..73452c42 100644 --- a/apps/api/src/scraper/WebScraper/utils/removeBase64Images.ts +++ b/apps/api/src/scraper/WebScraper/utils/removeBase64Images.ts @@ -1,7 +1,5 @@ -export const removeBase64Images = async ( - markdown: string, -) => { +export const removeBase64Images = async (markdown: string) => { const regex = /(!\[.*?\])\(data:image\/.*?;base64,.*?\)/g; - markdown = markdown.replace(regex, '$1()'); + markdown = markdown.replace(regex, "$1()"); return markdown; }; diff --git a/apps/api/src/scraper/scrapeURL/engines/cache/index.ts b/apps/api/src/scraper/scrapeURL/engines/cache/index.ts index 9506be0f..f6ffcb13 100644 --- a/apps/api/src/scraper/scrapeURL/engines/cache/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/cache/index.ts @@ -4,16 +4,16 @@ import { Meta } from "../.."; import { EngineError } from "../../error"; export async function scrapeCache(meta: Meta): Promise { - const key = cacheKey(meta.url, meta.options, meta.internalOptions); - if (key === null) throw new EngineError("Scrape not eligible for caching"); + const key = cacheKey(meta.url, meta.options, meta.internalOptions); + if (key === null) throw new EngineError("Scrape not eligible for caching"); - const entry = await getEntryFromCache(key); - if (entry === null) throw new EngineError("Cache missed"); + const entry = await getEntryFromCache(key); + if (entry === null) throw new EngineError("Cache missed"); - return { - url: entry.url, - html: entry.html, - statusCode: entry.statusCode, - error: entry.error, - }; -} \ No newline at end of file + return { + url: entry.url, + html: entry.html, + statusCode: entry.statusCode, + error: entry.error + }; +} diff --git a/apps/api/src/scraper/scrapeURL/engines/docx/index.ts b/apps/api/src/scraper/scrapeURL/engines/docx/index.ts index 9881fae7..02ed0c3f 100644 --- a/apps/api/src/scraper/scrapeURL/engines/docx/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/docx/index.ts @@ -4,12 +4,12 @@ import { downloadFile } from "../utils/downloadFile"; import mammoth from "mammoth"; export async function scrapeDOCX(meta: Meta): Promise { - const { response, tempFilePath } = await downloadFile(meta.id, meta.url); + const { response, tempFilePath } = await downloadFile(meta.id, meta.url); - return { - url: response.url, - statusCode: response.status, + return { + url: response.url, + statusCode: response.status, - html: (await mammoth.convertToHtml({ path: tempFilePath })).value, - } + html: (await mammoth.convertToHtml({ path: tempFilePath })).value + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts b/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts index 2c809901..92f2d451 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts @@ -3,26 +3,34 @@ import { Meta } from "../.."; import { TimeoutError } from "../../error"; import { specialtyScrapeCheck } from "../utils/specialtyHandler"; -export async function scrapeURLWithFetch(meta: Meta): Promise { - const timeout = 20000; +export async function scrapeURLWithFetch( + meta: Meta +): Promise { + const timeout = 20000; - const response = await Promise.race([ - fetch(meta.url, { - redirect: "follow", - headers: meta.options.headers, - }), - (async () => { - await new Promise((resolve) => setTimeout(() => resolve(null), timeout)); - throw new TimeoutError("Fetch was unable to scrape the page before timing out", { cause: { timeout } }); - })() - ]); + const response = await Promise.race([ + fetch(meta.url, { + redirect: "follow", + headers: meta.options.headers + }), + (async () => { + await new Promise((resolve) => setTimeout(() => resolve(null), timeout)); + throw new TimeoutError( + "Fetch was unable to scrape the page before timing out", + { cause: { timeout } } + ); + })() + ]); - specialtyScrapeCheck(meta.logger.child({ method: "scrapeURLWithFetch/specialtyScrapeCheck" }), Object.fromEntries(response.headers as any)); + specialtyScrapeCheck( + meta.logger.child({ method: "scrapeURLWithFetch/specialtyScrapeCheck" }), + Object.fromEntries(response.headers as any) + ); - return { - url: response.url, - html: await response.text(), - statusCode: response.status, - // TODO: error? - }; + return { + url: response.url, + html: await response.text(), + statusCode: response.status + // TODO: error? + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts index 53c19f3c..c3742d26 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts @@ -6,105 +6,132 @@ import { robustFetch } from "../../lib/fetch"; import { EngineError, SiteError } from "../../error"; const successSchema = z.object({ - jobId: z.string(), - state: z.literal("completed"), - processing: z.literal(false), + jobId: z.string(), + state: z.literal("completed"), + processing: z.literal(false), - // timeTaken: z.number(), - content: z.string(), - url: z.string().optional(), + // timeTaken: z.number(), + content: z.string(), + url: z.string().optional(), - pageStatusCode: z.number(), - pageError: z.string().optional(), + pageStatusCode: z.number(), + pageError: z.string().optional(), - // TODO: this needs to be non-optional, might need fixes on f-e side to ensure reliability - responseHeaders: z.record(z.string(), z.string()).optional(), + // TODO: this needs to be non-optional, might need fixes on f-e side to ensure reliability + responseHeaders: z.record(z.string(), z.string()).optional(), - // timeTakenCookie: z.number().optional(), - // timeTakenRequest: z.number().optional(), + // timeTakenCookie: z.number().optional(), + // timeTakenRequest: z.number().optional(), - // legacy: playwright only - screenshot: z.string().optional(), + // legacy: playwright only + screenshot: z.string().optional(), - // new: actions - screenshots: z.string().array().optional(), - actionContent: z.object({ - url: z.string(), - html: z.string(), - }).array().optional(), -}) + // new: actions + screenshots: z.string().array().optional(), + actionContent: z + .object({ + url: z.string(), + html: z.string() + }) + .array() + .optional() +}); export type FireEngineCheckStatusSuccess = z.infer; const processingSchema = z.object({ - jobId: z.string(), - state: z.enum(["delayed", "active", "waiting", "waiting-children", "unknown", "prioritized"]), - processing: z.boolean(), + jobId: z.string(), + state: z.enum([ + "delayed", + "active", + "waiting", + "waiting-children", + "unknown", + "prioritized" + ]), + processing: z.boolean() }); const failedSchema = z.object({ - jobId: z.string(), - state: z.literal("failed"), - processing: z.literal(false), - error: z.string(), + jobId: z.string(), + state: z.literal("failed"), + processing: z.literal(false), + error: z.string() }); export class StillProcessingError extends Error { - constructor(jobId: string) { - super("Job is still under processing", { cause: { jobId } }) - } + constructor(jobId: string) { + super("Job is still under processing", { cause: { jobId } }); + } } -export async function fireEngineCheckStatus(logger: Logger, jobId: string): Promise { - const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; +export async function fireEngineCheckStatus( + logger: Logger, + jobId: string +): Promise { + const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; - const status = await Sentry.startSpan({ - name: "fire-engine: Check status", - attributes: { - jobId, + const status = await Sentry.startSpan( + { + name: "fire-engine: Check status", + attributes: { + jobId + } + }, + async (span) => { + return await robustFetch({ + url: `${fireEngineURL}/scrape/${jobId}`, + method: "GET", + logger: logger.child({ method: "fireEngineCheckStatus/robustFetch" }), + headers: { + ...(Sentry.isInitialized() + ? { + "sentry-trace": Sentry.spanToTraceHeader(span), + baggage: Sentry.spanToBaggageHeader(span) + } + : {}) } - }, async span => { - return await robustFetch( - { - url: `${fireEngineURL}/scrape/${jobId}`, - method: "GET", - logger: logger.child({ method: "fireEngineCheckStatus/robustFetch" }), - headers: { - ...(Sentry.isInitialized() ? ({ - "sentry-trace": Sentry.spanToTraceHeader(span), - "baggage": Sentry.spanToBaggageHeader(span), - }) : {}), - }, - } - ) - }); + }); + } + ); - const successParse = successSchema.safeParse(status); - const processingParse = processingSchema.safeParse(status); - const failedParse = failedSchema.safeParse(status); + const successParse = successSchema.safeParse(status); + const processingParse = processingSchema.safeParse(status); + const failedParse = failedSchema.safeParse(status); - if (successParse.success) { - logger.debug("Scrape succeeded!", { jobId }); - return successParse.data; - } else if (processingParse.success) { - throw new StillProcessingError(jobId); - } else if (failedParse.success) { - logger.debug("Scrape job failed", { status, jobId }); - if (typeof status.error === "string" && status.error.includes("Chrome error: ")) { - throw new SiteError(status.error.split("Chrome error: ")[1]); - } else { - throw new EngineError("Scrape job failed", { - cause: { - status, jobId - } - }); - } + if (successParse.success) { + logger.debug("Scrape succeeded!", { jobId }); + return successParse.data; + } else if (processingParse.success) { + throw new StillProcessingError(jobId); + } else if (failedParse.success) { + logger.debug("Scrape job failed", { status, jobId }); + if ( + typeof status.error === "string" && + status.error.includes("Chrome error: ") + ) { + throw new SiteError(status.error.split("Chrome error: ")[1]); } else { - logger.debug("Check status returned response not matched by any schema", { status, jobId }); - throw new Error("Check status returned response not matched by any schema", { - cause: { - status, jobId - } - }); + throw new EngineError("Scrape job failed", { + cause: { + status, + jobId + } + }); } + } else { + logger.debug("Check status returned response not matched by any schema", { + status, + jobId + }); + throw new Error( + "Check status returned response not matched by any schema", + { + cause: { + status, + jobId + } + } + ); + } } diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts index ed07be88..96d73390 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts @@ -4,30 +4,33 @@ import * as Sentry from "@sentry/node"; import { robustFetch } from "../../lib/fetch"; export async function fireEngineDelete(logger: Logger, jobId: string) { - const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; + const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; - await Sentry.startSpan({ - name: "fire-engine: Delete scrape", - attributes: { - jobId, - } - }, async span => { - await robustFetch( - { - url: `${fireEngineURL}/scrape/${jobId}`, - method: "DELETE", - headers: { - ...(Sentry.isInitialized() ? ({ - "sentry-trace": Sentry.spanToTraceHeader(span), - "baggage": Sentry.spanToBaggageHeader(span), - }) : {}), - }, - ignoreResponse: true, - ignoreFailure: true, - logger: logger.child({ method: "fireEngineDelete/robustFetch", jobId }), - } - ) - }); + await Sentry.startSpan( + { + name: "fire-engine: Delete scrape", + attributes: { + jobId + } + }, + async (span) => { + await robustFetch({ + url: `${fireEngineURL}/scrape/${jobId}`, + method: "DELETE", + headers: { + ...(Sentry.isInitialized() + ? { + "sentry-trace": Sentry.spanToTraceHeader(span), + baggage: Sentry.spanToBaggageHeader(span) + } + : {}) + }, + ignoreResponse: true, + ignoreFailure: true, + logger: logger.child({ method: "fireEngineDelete/robustFetch", jobId }) + }); + } + ); - // We do not care whether this fails or not. -} \ No newline at end of file + // We do not care whether this fails or not. +} diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index ae953c1b..851b8faf 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -1,8 +1,18 @@ import { Logger } from "winston"; import { Meta } from "../.."; -import { fireEngineScrape, FireEngineScrapeRequestChromeCDP, FireEngineScrapeRequestCommon, FireEngineScrapeRequestPlaywright, FireEngineScrapeRequestTLSClient } from "./scrape"; +import { + fireEngineScrape, + FireEngineScrapeRequestChromeCDP, + FireEngineScrapeRequestCommon, + FireEngineScrapeRequestPlaywright, + FireEngineScrapeRequestTLSClient +} from "./scrape"; import { EngineScrapeResult } from ".."; -import { fireEngineCheckStatus, FireEngineCheckStatusSuccess, StillProcessingError } from "./checkStatus"; +import { + fireEngineCheckStatus, + FireEngineCheckStatusSuccess, + StillProcessingError +} from "./checkStatus"; import { EngineError, SiteError, TimeoutError } from "../../error"; import * as Sentry from "@sentry/node"; import { Action } from "../../../../lib/entities"; @@ -13,203 +23,293 @@ export const defaultTimeout = 10000; // This function does not take `Meta` on purpose. It may not access any // meta values to construct the request -- that must be done by the // `scrapeURLWithFireEngine*` functions. -async function performFireEngineScrape( - logger: Logger, - request: FireEngineScrapeRequestCommon & Engine, - timeout = defaultTimeout, +async function performFireEngineScrape< + Engine extends + | FireEngineScrapeRequestChromeCDP + | FireEngineScrapeRequestPlaywright + | FireEngineScrapeRequestTLSClient +>( + logger: Logger, + request: FireEngineScrapeRequestCommon & Engine, + timeout = defaultTimeout ): Promise { - const scrape = await fireEngineScrape(logger.child({ method: "fireEngineScrape" }), request); + const scrape = await fireEngineScrape( + logger.child({ method: "fireEngineScrape" }), + request + ); - const startTime = Date.now(); - const errorLimit = 3; - let errors: any[] = []; - let status: FireEngineCheckStatusSuccess | undefined = undefined; + const startTime = Date.now(); + const errorLimit = 3; + let errors: any[] = []; + let status: FireEngineCheckStatusSuccess | undefined = undefined; - while (status === undefined) { - if (errors.length >= errorLimit) { - logger.error("Error limit hit.", { errors }); - throw new Error("Error limit hit. See e.cause.errors for errors.", { cause: { errors } }); - } - - if (Date.now() - startTime > timeout) { - logger.info("Fire-engine was unable to scrape the page before timing out.", { errors, timeout }); - throw new TimeoutError("Fire-engine was unable to scrape the page before timing out", { cause: { errors, timeout } }); - } - - try { - status = await fireEngineCheckStatus(logger.child({ method: "fireEngineCheckStatus" }), scrape.jobId) - } catch (error) { - if (error instanceof StillProcessingError) { - // nop - } else if (error instanceof EngineError || error instanceof SiteError) { - logger.debug("Fire-engine scrape job failed.", { error, jobId: scrape.jobId }); - throw error; - } else { - Sentry.captureException(error); - errors.push(error); - logger.debug(`An unexpeceted error occurred while calling checkStatus. Error counter is now at ${errors.length}.`, { error, jobId: scrape.jobId }); - } - } - - await new Promise((resolve) => setTimeout(resolve, 250)); + while (status === undefined) { + if (errors.length >= errorLimit) { + logger.error("Error limit hit.", { errors }); + throw new Error("Error limit hit. See e.cause.errors for errors.", { + cause: { errors } + }); } - return status; + if (Date.now() - startTime > timeout) { + logger.info( + "Fire-engine was unable to scrape the page before timing out.", + { errors, timeout } + ); + throw new TimeoutError( + "Fire-engine was unable to scrape the page before timing out", + { cause: { errors, timeout } } + ); + } + + try { + status = await fireEngineCheckStatus( + logger.child({ method: "fireEngineCheckStatus" }), + scrape.jobId + ); + } catch (error) { + if (error instanceof StillProcessingError) { + // nop + } else if (error instanceof EngineError || error instanceof SiteError) { + logger.debug("Fire-engine scrape job failed.", { + error, + jobId: scrape.jobId + }); + throw error; + } else { + Sentry.captureException(error); + errors.push(error); + logger.debug( + `An unexpeceted error occurred while calling checkStatus. Error counter is now at ${errors.length}.`, + { error, jobId: scrape.jobId } + ); + } + } + + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + return status; } -export async function scrapeURLWithFireEngineChromeCDP(meta: Meta): Promise { - const actions: Action[] = [ - // Transform waitFor option into an action (unsupported by chrome-cdp) - ...(meta.options.waitFor !== 0 ? [{ +export async function scrapeURLWithFireEngineChromeCDP( + meta: Meta +): Promise { + const actions: Action[] = [ + // Transform waitFor option into an action (unsupported by chrome-cdp) + ...(meta.options.waitFor !== 0 + ? [ + { type: "wait" as const, - milliseconds: meta.options.waitFor, - }] : []), + milliseconds: meta.options.waitFor + } + ] + : []), - // Transform screenshot format into an action (unsupported by chrome-cdp) - ...(meta.options.formats.includes("screenshot") || meta.options.formats.includes("screenshot@fullPage") ? [{ + // Transform screenshot format into an action (unsupported by chrome-cdp) + ...(meta.options.formats.includes("screenshot") || + meta.options.formats.includes("screenshot@fullPage") + ? [ + { type: "screenshot" as const, - fullPage: meta.options.formats.includes("screenshot@fullPage"), - }] : []), + fullPage: meta.options.formats.includes("screenshot@fullPage") + } + ] + : []), - // Include specified actions - ...(meta.options.actions ?? []), - ]; + // Include specified actions + ...(meta.options.actions ?? []) + ]; - const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestChromeCDP = { - url: meta.url, - engine: "chrome-cdp", - instantReturn: true, - skipTlsVerification: meta.options.skipTlsVerification, - headers: meta.options.headers, - ...(actions.length > 0 ? ({ - actions, - }) : {}), - priority: meta.internalOptions.priority, - geolocation: meta.options.geolocation, - mobile: meta.options.mobile, - timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic - disableSmartWaitCache: meta.internalOptions.disableSmartWaitCache, - // TODO: scrollXPaths - }; + const request: FireEngineScrapeRequestCommon & + FireEngineScrapeRequestChromeCDP = { + url: meta.url, + engine: "chrome-cdp", + instantReturn: true, + skipTlsVerification: meta.options.skipTlsVerification, + headers: meta.options.headers, + ...(actions.length > 0 + ? { + actions + } + : {}), + priority: meta.internalOptions.priority, + geolocation: meta.options.geolocation, + mobile: meta.options.mobile, + timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic + disableSmartWaitCache: meta.internalOptions.disableSmartWaitCache + // TODO: scrollXPaths + }; - const totalWait = actions.reduce((a,x) => x.type === "wait" ? (x.milliseconds ?? 1000) + a : a, 0); + const totalWait = actions.reduce( + (a, x) => (x.type === "wait" ? (x.milliseconds ?? 1000) + a : a), + 0 + ); - let response = await performFireEngineScrape( - meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", request }), - request, - meta.options.timeout !== undefined - ? defaultTimeout + totalWait - : Infinity, // TODO: better timeout handling + let response = await performFireEngineScrape( + meta.logger.child({ + method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", + request + }), + request, + meta.options.timeout !== undefined ? defaultTimeout + totalWait : Infinity // TODO: better timeout handling + ); + + specialtyScrapeCheck( + meta.logger.child({ + method: "scrapeURLWithFireEngineChromeCDP/specialtyScrapeCheck" + }), + response.responseHeaders + ); + + if ( + meta.options.formats.includes("screenshot") || + meta.options.formats.includes("screenshot@fullPage") + ) { + meta.logger.debug( + "Transforming screenshots from actions into screenshot field", + { screenshots: response.screenshots } ); + response.screenshot = (response.screenshots ?? [])[0]; + (response.screenshots ?? []).splice(0, 1); + meta.logger.debug("Screenshot transformation done", { + screenshots: response.screenshots, + screenshot: response.screenshot + }); + } - specialtyScrapeCheck(meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/specialtyScrapeCheck" }), response.responseHeaders); + if (!response.url) { + meta.logger.warn("Fire-engine did not return the response's URL", { + response, + sourceURL: meta.url + }); + } - if (meta.options.formats.includes("screenshot") || meta.options.formats.includes("screenshot@fullPage")) { - meta.logger.debug("Transforming screenshots from actions into screenshot field", { screenshots: response.screenshots }); - response.screenshot = (response.screenshots ?? [])[0]; - (response.screenshots ?? []).splice(0, 1); - meta.logger.debug("Screenshot transformation done", { screenshots: response.screenshots, screenshot: response.screenshot }); - } + return { + url: response.url ?? meta.url, - if (!response.url) { - meta.logger.warn("Fire-engine did not return the response's URL", { response, sourceURL: meta.url }); - } + html: response.content, + error: response.pageError, + statusCode: response.pageStatusCode, - return { - url: response.url ?? meta.url, - - html: response.content, - error: response.pageError, - statusCode: response.pageStatusCode, - - screenshot: response.screenshot, - ...(actions.length > 0 ? { - actions: { - screenshots: response.screenshots ?? [], - scrapes: response.actionContent ?? [], - } - } : {}), - }; + screenshot: response.screenshot, + ...(actions.length > 0 + ? { + actions: { + screenshots: response.screenshots ?? [], + scrapes: response.actionContent ?? [] + } + } + : {}) + }; } -export async function scrapeURLWithFireEnginePlaywright(meta: Meta): Promise { - const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestPlaywright = { - url: meta.url, - engine: "playwright", - instantReturn: true, +export async function scrapeURLWithFireEnginePlaywright( + meta: Meta +): Promise { + const request: FireEngineScrapeRequestCommon & + FireEngineScrapeRequestPlaywright = { + url: meta.url, + engine: "playwright", + instantReturn: true, - headers: meta.options.headers, - priority: meta.internalOptions.priority, - screenshot: meta.options.formats.includes("screenshot"), - fullPageScreenshot: meta.options.formats.includes("screenshot@fullPage"), - wait: meta.options.waitFor, - geolocation: meta.options.geolocation, + headers: meta.options.headers, + priority: meta.internalOptions.priority, + screenshot: meta.options.formats.includes("screenshot"), + fullPageScreenshot: meta.options.formats.includes("screenshot@fullPage"), + wait: meta.options.waitFor, + geolocation: meta.options.geolocation, - timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic - }; + timeout: meta.options.timeout === undefined ? 300000 : undefined // TODO: better timeout logic + }; - let response = await performFireEngineScrape( - meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", request }), - request, - meta.options.timeout !== undefined - ? defaultTimeout + meta.options.waitFor - : Infinity, // TODO: better timeout handling - ); - - specialtyScrapeCheck(meta.logger.child({ method: "scrapeURLWithFireEnginePlaywright/specialtyScrapeCheck" }), response.responseHeaders); + let response = await performFireEngineScrape( + meta.logger.child({ + method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", + request + }), + request, + meta.options.timeout !== undefined + ? defaultTimeout + meta.options.waitFor + : Infinity // TODO: better timeout handling + ); - if (!response.url) { - meta.logger.warn("Fire-engine did not return the response's URL", { response, sourceURL: meta.url }); - } + specialtyScrapeCheck( + meta.logger.child({ + method: "scrapeURLWithFireEnginePlaywright/specialtyScrapeCheck" + }), + response.responseHeaders + ); - return { - url: response.url ?? meta.url, + if (!response.url) { + meta.logger.warn("Fire-engine did not return the response's URL", { + response, + sourceURL: meta.url + }); + } - html: response.content, - error: response.pageError, - statusCode: response.pageStatusCode, + return { + url: response.url ?? meta.url, - ...(response.screenshots !== undefined && response.screenshots.length > 0 ? ({ - screenshot: response.screenshots[0], - }) : {}), - }; + html: response.content, + error: response.pageError, + statusCode: response.pageStatusCode, + + ...(response.screenshots !== undefined && response.screenshots.length > 0 + ? { + screenshot: response.screenshots[0] + } + : {}) + }; } -export async function scrapeURLWithFireEngineTLSClient(meta: Meta): Promise { - const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestTLSClient = { - url: meta.url, - engine: "tlsclient", - instantReturn: true, +export async function scrapeURLWithFireEngineTLSClient( + meta: Meta +): Promise { + const request: FireEngineScrapeRequestCommon & + FireEngineScrapeRequestTLSClient = { + url: meta.url, + engine: "tlsclient", + instantReturn: true, - headers: meta.options.headers, - priority: meta.internalOptions.priority, + headers: meta.options.headers, + priority: meta.internalOptions.priority, - atsv: meta.internalOptions.atsv, - geolocation: meta.options.geolocation, - disableJsDom: meta.internalOptions.v0DisableJsDom, + atsv: meta.internalOptions.atsv, + geolocation: meta.options.geolocation, + disableJsDom: meta.internalOptions.v0DisableJsDom, - timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic - }; + timeout: meta.options.timeout === undefined ? 300000 : undefined // TODO: better timeout logic + }; - let response = await performFireEngineScrape( - meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", request }), - request, - meta.options.timeout !== undefined - ? defaultTimeout - : Infinity, // TODO: better timeout handling - ); + let response = await performFireEngineScrape( + meta.logger.child({ + method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", + request + }), + request, + meta.options.timeout !== undefined ? defaultTimeout : Infinity // TODO: better timeout handling + ); - specialtyScrapeCheck(meta.logger.child({ method: "scrapeURLWithFireEngineTLSClient/specialtyScrapeCheck" }), response.responseHeaders); + specialtyScrapeCheck( + meta.logger.child({ + method: "scrapeURLWithFireEngineTLSClient/specialtyScrapeCheck" + }), + response.responseHeaders + ); - if (!response.url) { - meta.logger.warn("Fire-engine did not return the response's URL", { response, sourceURL: meta.url }); - } + if (!response.url) { + meta.logger.warn("Fire-engine did not return the response's URL", { + response, + sourceURL: meta.url + }); + } - return { - url: response.url ?? meta.url, + return { + url: response.url ?? meta.url, - html: response.content, - error: response.pageError, - statusCode: response.pageStatusCode, - }; + html: response.content, + error: response.pageError, + statusCode: response.pageStatusCode + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts index 6efb4348..ffca4b41 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts @@ -6,92 +6,100 @@ import { Action } from "../../../../lib/entities"; import { robustFetch } from "../../lib/fetch"; export type FireEngineScrapeRequestCommon = { - url: string; - - headers?: { [K: string]: string }; + url: string; - blockMedia?: boolean; // default: true - blockAds?: boolean; // default: true - // pageOptions?: any; // unused, .scrollXPaths is considered on FE side + headers?: { [K: string]: string }; - // useProxy?: boolean; // unused, default: true - // customProxy?: string; // unused + blockMedia?: boolean; // default: true + blockAds?: boolean; // default: true + // pageOptions?: any; // unused, .scrollXPaths is considered on FE side - // disableSmartWaitCache?: boolean; // unused, default: false - // skipDnsCheck?: boolean; // unused, default: false + // useProxy?: boolean; // unused, default: true + // customProxy?: string; // unused - priority?: number; // default: 1 - // team_id?: string; // unused - logRequest?: boolean; // default: true - instantReturn?: boolean; // default: false - geolocation?: { country?: string; languages?: string[]; }; + // disableSmartWaitCache?: boolean; // unused, default: false + // skipDnsCheck?: boolean; // unused, default: false - timeout?: number; -} + priority?: number; // default: 1 + // team_id?: string; // unused + logRequest?: boolean; // default: true + instantReturn?: boolean; // default: false + geolocation?: { country?: string; languages?: string[] }; + + timeout?: number; +}; export type FireEngineScrapeRequestChromeCDP = { - engine: "chrome-cdp"; - skipTlsVerification?: boolean; - actions?: Action[]; - blockMedia?: true; // cannot be false - mobile?: boolean; - disableSmartWaitCache?: boolean; + engine: "chrome-cdp"; + skipTlsVerification?: boolean; + actions?: Action[]; + blockMedia?: true; // cannot be false + mobile?: boolean; + disableSmartWaitCache?: boolean; }; export type FireEngineScrapeRequestPlaywright = { - engine: "playwright"; - blockAds?: boolean; // default: true + engine: "playwright"; + blockAds?: boolean; // default: true - // mutually exclusive, default: false - screenshot?: boolean; - fullPageScreenshot?: boolean; + // mutually exclusive, default: false + screenshot?: boolean; + fullPageScreenshot?: boolean; - wait?: number; // default: 0 + wait?: number; // default: 0 }; export type FireEngineScrapeRequestTLSClient = { - engine: "tlsclient"; - atsv?: boolean; // v0 only, default: false - disableJsDom?: boolean; // v0 only, default: false - // blockAds?: boolean; // default: true + engine: "tlsclient"; + atsv?: boolean; // v0 only, default: false + disableJsDom?: boolean; // v0 only, default: false + // blockAds?: boolean; // default: true }; const schema = z.object({ - jobId: z.string(), - processing: z.boolean(), + jobId: z.string(), + processing: z.boolean() }); -export async function fireEngineScrape ( - logger: Logger, - request: FireEngineScrapeRequestCommon & Engine, +export async function fireEngineScrape< + Engine extends + | FireEngineScrapeRequestChromeCDP + | FireEngineScrapeRequestPlaywright + | FireEngineScrapeRequestTLSClient +>( + logger: Logger, + request: FireEngineScrapeRequestCommon & Engine ): Promise> { - const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; + const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; - // TODO: retries + // TODO: retries - const scrapeRequest = await Sentry.startSpan({ - name: "fire-engine: Scrape", - attributes: { - url: request.url, + const scrapeRequest = await Sentry.startSpan( + { + name: "fire-engine: Scrape", + attributes: { + url: request.url + } + }, + async (span) => { + return await robustFetch({ + url: `${fireEngineURL}/scrape`, + method: "POST", + headers: { + ...(Sentry.isInitialized() + ? { + "sentry-trace": Sentry.spanToTraceHeader(span), + baggage: Sentry.spanToBaggageHeader(span) + } + : {}) }, - }, async span => { - return await robustFetch( - { - url: `${fireEngineURL}/scrape`, - method: "POST", - headers: { - ...(Sentry.isInitialized() ? ({ - "sentry-trace": Sentry.spanToTraceHeader(span), - "baggage": Sentry.spanToBaggageHeader(span), - }) : {}), - }, - body: request, - logger: logger.child({ method: "fireEngineScrape/robustFetch" }), - schema, - tryCount: 3, - } - ); - }); + body: request, + logger: logger.child({ method: "fireEngineScrape/robustFetch" }), + schema, + tryCount: 3 + }); + } + ); - return scrapeRequest; -} \ No newline at end of file + return scrapeRequest; +} diff --git a/apps/api/src/scraper/scrapeURL/engines/index.ts b/apps/api/src/scraper/scrapeURL/engines/index.ts index 8a8f4476..1d9db249 100644 --- a/apps/api/src/scraper/scrapeURL/engines/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/index.ts @@ -1,316 +1,387 @@ import { ScrapeActionContent } from "../../../lib/entities"; import { Meta } from ".."; import { scrapeDOCX } from "./docx"; -import { scrapeURLWithFireEngineChromeCDP, scrapeURLWithFireEnginePlaywright, scrapeURLWithFireEngineTLSClient } from "./fire-engine"; +import { + scrapeURLWithFireEngineChromeCDP, + scrapeURLWithFireEnginePlaywright, + scrapeURLWithFireEngineTLSClient +} from "./fire-engine"; import { scrapePDF } from "./pdf"; import { scrapeURLWithScrapingBee } from "./scrapingbee"; import { scrapeURLWithFetch } from "./fetch"; import { scrapeURLWithPlaywright } from "./playwright"; import { scrapeCache } from "./cache"; -export type Engine = "fire-engine;chrome-cdp" | "fire-engine;playwright" | "fire-engine;tlsclient" | "scrapingbee" | "scrapingbeeLoad" | "playwright" | "fetch" | "pdf" | "docx" | "cache"; - -const useScrapingBee = process.env.SCRAPING_BEE_API_KEY !== '' && process.env.SCRAPING_BEE_API_KEY !== undefined; -const useFireEngine = process.env.FIRE_ENGINE_BETA_URL !== '' && process.env.FIRE_ENGINE_BETA_URL !== undefined; -const usePlaywright = process.env.PLAYWRIGHT_MICROSERVICE_URL !== '' && process.env.PLAYWRIGHT_MICROSERVICE_URL !== undefined; -const useCache = process.env.CACHE_REDIS_URL !== '' && process.env.CACHE_REDIS_URL !== undefined; +export type Engine = + | "fire-engine;chrome-cdp" + | "fire-engine;playwright" + | "fire-engine;tlsclient" + | "scrapingbee" + | "scrapingbeeLoad" + | "playwright" + | "fetch" + | "pdf" + | "docx" + | "cache"; +const useScrapingBee = + process.env.SCRAPING_BEE_API_KEY !== "" && + process.env.SCRAPING_BEE_API_KEY !== undefined; +const useFireEngine = + process.env.FIRE_ENGINE_BETA_URL !== "" && + process.env.FIRE_ENGINE_BETA_URL !== undefined; +const usePlaywright = + process.env.PLAYWRIGHT_MICROSERVICE_URL !== "" && + process.env.PLAYWRIGHT_MICROSERVICE_URL !== undefined; +const useCache = + process.env.CACHE_REDIS_URL !== "" && + process.env.CACHE_REDIS_URL !== undefined; export const engines: Engine[] = [ - // ...(useCache ? [ "cache" as const ] : []), - ...(useFireEngine ? [ "fire-engine;chrome-cdp" as const, "fire-engine;playwright" as const, "fire-engine;tlsclient" as const ] : []), - ...(useScrapingBee ? [ "scrapingbee" as const, "scrapingbeeLoad" as const ] : []), - ...(usePlaywright ? [ "playwright" as const ] : []), - "fetch", - "pdf", - "docx", + // ...(useCache ? [ "cache" as const ] : []), + ...(useFireEngine + ? [ + "fire-engine;chrome-cdp" as const, + "fire-engine;playwright" as const, + "fire-engine;tlsclient" as const + ] + : []), + ...(useScrapingBee + ? ["scrapingbee" as const, "scrapingbeeLoad" as const] + : []), + ...(usePlaywright ? ["playwright" as const] : []), + "fetch", + "pdf", + "docx" ]; export const featureFlags = [ - "actions", - "waitFor", - "screenshot", - "screenshot@fullScreen", - "pdf", - "docx", - "atsv", - "location", - "mobile", - "skipTlsVerification", - "useFastMode", + "actions", + "waitFor", + "screenshot", + "screenshot@fullScreen", + "pdf", + "docx", + "atsv", + "location", + "mobile", + "skipTlsVerification", + "useFastMode" ] as const; -export type FeatureFlag = typeof featureFlags[number]; +export type FeatureFlag = (typeof featureFlags)[number]; export const featureFlagOptions: { - [F in FeatureFlag]: { - priority: number; - } + [F in FeatureFlag]: { + priority: number; + }; } = { - "actions": { priority: 20 }, - "waitFor": { priority: 1 }, - "screenshot": { priority: 10 }, - "screenshot@fullScreen": { priority: 10 }, - "pdf": { priority: 100 }, - "docx": { priority: 100 }, - "atsv": { priority: 90 }, // NOTE: should atsv force to tlsclient? adjust priority if not - "useFastMode": { priority: 90 }, - "location": { priority: 10 }, - "mobile": { priority: 10 }, - "skipTlsVerification": { priority: 10 }, + actions: { priority: 20 }, + waitFor: { priority: 1 }, + screenshot: { priority: 10 }, + "screenshot@fullScreen": { priority: 10 }, + pdf: { priority: 100 }, + docx: { priority: 100 }, + atsv: { priority: 90 }, // NOTE: should atsv force to tlsclient? adjust priority if not + useFastMode: { priority: 90 }, + location: { priority: 10 }, + mobile: { priority: 10 }, + skipTlsVerification: { priority: 10 } } as const; export type EngineScrapeResult = { - url: string; + url: string; - html: string; - markdown?: string; - statusCode: number; - error?: string; + html: string; + markdown?: string; + statusCode: number; + error?: string; - screenshot?: string; - actions?: { - screenshots: string[]; - scrapes: ScrapeActionContent[]; - }; -} + screenshot?: string; + actions?: { + screenshots: string[]; + scrapes: ScrapeActionContent[]; + }; +}; const engineHandlers: { - [E in Engine]: (meta: Meta) => Promise + [E in Engine]: (meta: Meta) => Promise; } = { - "cache": scrapeCache, - "fire-engine;chrome-cdp": scrapeURLWithFireEngineChromeCDP, - "fire-engine;playwright": scrapeURLWithFireEnginePlaywright, - "fire-engine;tlsclient": scrapeURLWithFireEngineTLSClient, - "scrapingbee": scrapeURLWithScrapingBee("domcontentloaded"), - "scrapingbeeLoad": scrapeURLWithScrapingBee("networkidle2"), - "playwright": scrapeURLWithPlaywright, - "fetch": scrapeURLWithFetch, - "pdf": scrapePDF, - "docx": scrapeDOCX, + cache: scrapeCache, + "fire-engine;chrome-cdp": scrapeURLWithFireEngineChromeCDP, + "fire-engine;playwright": scrapeURLWithFireEnginePlaywright, + "fire-engine;tlsclient": scrapeURLWithFireEngineTLSClient, + scrapingbee: scrapeURLWithScrapingBee("domcontentloaded"), + scrapingbeeLoad: scrapeURLWithScrapingBee("networkidle2"), + playwright: scrapeURLWithPlaywright, + fetch: scrapeURLWithFetch, + pdf: scrapePDF, + docx: scrapeDOCX }; export const engineOptions: { - [E in Engine]: { - // A list of feature flags the engine supports. - features: { [F in FeatureFlag]: boolean }, + [E in Engine]: { + // A list of feature flags the engine supports. + features: { [F in FeatureFlag]: boolean }; - // This defines the order of engines in general. The engine with the highest quality will be used the most. - // Negative quality numbers are reserved for specialty engines, e.g. PDF and DOCX - quality: number, - } + // This defines the order of engines in general. The engine with the highest quality will be used the most. + // Negative quality numbers are reserved for specialty engines, e.g. PDF and DOCX + quality: number; + }; } = { - "cache": { - features: { - "actions": false, - "waitFor": true, - "screenshot": false, - "screenshot@fullScreen": false, - "pdf": false, // TODO: figure this out - "docx": false, // TODO: figure this out - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": false, - }, - quality: 1000, // cache should always be tried first + cache: { + features: { + actions: false, + waitFor: true, + screenshot: false, + "screenshot@fullScreen": false, + pdf: false, // TODO: figure this out + docx: false, // TODO: figure this out + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: false }, - "fire-engine;chrome-cdp": { - features: { - "actions": true, - "waitFor": true, // through actions transform - "screenshot": true, // through actions transform - "screenshot@fullScreen": true, // through actions transform - "pdf": false, - "docx": false, - "atsv": false, - "location": true, - "mobile": true, - "skipTlsVerification": true, - "useFastMode": false, - }, - quality: 50, + quality: 1000 // cache should always be tried first + }, + "fire-engine;chrome-cdp": { + features: { + actions: true, + waitFor: true, // through actions transform + screenshot: true, // through actions transform + "screenshot@fullScreen": true, // through actions transform + pdf: false, + docx: false, + atsv: false, + location: true, + mobile: true, + skipTlsVerification: true, + useFastMode: false }, - "fire-engine;playwright": { - features: { - "actions": false, - "waitFor": true, - "screenshot": true, - "screenshot@fullScreen": true, - "pdf": false, - "docx": false, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": false, - }, - quality: 40, + quality: 50 + }, + "fire-engine;playwright": { + features: { + actions: false, + waitFor: true, + screenshot: true, + "screenshot@fullScreen": true, + pdf: false, + docx: false, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: false }, - "scrapingbee": { - features: { - "actions": false, - "waitFor": true, - "screenshot": true, - "screenshot@fullScreen": true, - "pdf": false, - "docx": false, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": false, - }, - quality: 30, + quality: 40 + }, + scrapingbee: { + features: { + actions: false, + waitFor: true, + screenshot: true, + "screenshot@fullScreen": true, + pdf: false, + docx: false, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: false }, - "scrapingbeeLoad": { - features: { - "actions": false, - "waitFor": true, - "screenshot": true, - "screenshot@fullScreen": true, - "pdf": false, - "docx": false, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": false, - }, - quality: 29, + quality: 30 + }, + scrapingbeeLoad: { + features: { + actions: false, + waitFor: true, + screenshot: true, + "screenshot@fullScreen": true, + pdf: false, + docx: false, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: false }, - "playwright": { - features: { - "actions": false, - "waitFor": true, - "screenshot": false, - "screenshot@fullScreen": false, - "pdf": false, - "docx": false, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": false, - }, - quality: 20, + quality: 29 + }, + playwright: { + features: { + actions: false, + waitFor: true, + screenshot: false, + "screenshot@fullScreen": false, + pdf: false, + docx: false, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: false }, - "fire-engine;tlsclient": { - features: { - "actions": false, - "waitFor": false, - "screenshot": false, - "screenshot@fullScreen": false, - "pdf": false, - "docx": false, - "atsv": true, - "location": true, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": true, - }, - quality: 10, + quality: 20 + }, + "fire-engine;tlsclient": { + features: { + actions: false, + waitFor: false, + screenshot: false, + "screenshot@fullScreen": false, + pdf: false, + docx: false, + atsv: true, + location: true, + mobile: false, + skipTlsVerification: false, + useFastMode: true }, - "fetch": { - features: { - "actions": false, - "waitFor": false, - "screenshot": false, - "screenshot@fullScreen": false, - "pdf": false, - "docx": false, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": true, - }, - quality: 5, + quality: 10 + }, + fetch: { + features: { + actions: false, + waitFor: false, + screenshot: false, + "screenshot@fullScreen": false, + pdf: false, + docx: false, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: true }, - "pdf": { - features: { - "actions": false, - "waitFor": false, - "screenshot": false, - "screenshot@fullScreen": false, - "pdf": true, - "docx": false, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": true, - }, - quality: -10, + quality: 5 + }, + pdf: { + features: { + actions: false, + waitFor: false, + screenshot: false, + "screenshot@fullScreen": false, + pdf: true, + docx: false, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: true }, - "docx": { - features: { - "actions": false, - "waitFor": false, - "screenshot": false, - "screenshot@fullScreen": false, - "pdf": false, - "docx": true, - "atsv": false, - "location": false, - "mobile": false, - "skipTlsVerification": false, - "useFastMode": true, - }, - quality: -10, + quality: -10 + }, + docx: { + features: { + actions: false, + waitFor: false, + screenshot: false, + "screenshot@fullScreen": false, + pdf: false, + docx: true, + atsv: false, + location: false, + mobile: false, + skipTlsVerification: false, + useFastMode: true }, + quality: -10 + } }; export function buildFallbackList(meta: Meta): { - engine: Engine, - unsupportedFeatures: Set, + engine: Engine; + unsupportedFeatures: Set; }[] { - const prioritySum = [...meta.featureFlags].reduce((a, x) => a + featureFlagOptions[x].priority, 0); - const priorityThreshold = Math.floor(prioritySum / 2); - let selectedEngines: { - engine: Engine, - supportScore: number, - unsupportedFeatures: Set, - }[] = []; + const prioritySum = [...meta.featureFlags].reduce( + (a, x) => a + featureFlagOptions[x].priority, + 0 + ); + const priorityThreshold = Math.floor(prioritySum / 2); + let selectedEngines: { + engine: Engine; + supportScore: number; + unsupportedFeatures: Set; + }[] = []; - const currentEngines = meta.internalOptions.forceEngine !== undefined ? [meta.internalOptions.forceEngine] : engines; + const currentEngines = + meta.internalOptions.forceEngine !== undefined + ? [meta.internalOptions.forceEngine] + : engines; - for (const engine of currentEngines) { - const supportedFlags = new Set([...Object.entries(engineOptions[engine].features).filter(([k, v]) => meta.featureFlags.has(k as FeatureFlag) && v === true).map(([k, _]) => k)]); - const supportScore = [...supportedFlags].reduce((a, x) => a + featureFlagOptions[x].priority, 0); + for (const engine of currentEngines) { + const supportedFlags = new Set([ + ...Object.entries(engineOptions[engine].features) + .filter( + ([k, v]) => meta.featureFlags.has(k as FeatureFlag) && v === true + ) + .map(([k, _]) => k) + ]); + const supportScore = [...supportedFlags].reduce( + (a, x) => a + featureFlagOptions[x].priority, + 0 + ); - const unsupportedFeatures = new Set([...meta.featureFlags]); - for (const flag of meta.featureFlags) { - if (supportedFlags.has(flag)) { - unsupportedFeatures.delete(flag); - } - } + const unsupportedFeatures = new Set([...meta.featureFlags]); + for (const flag of meta.featureFlags) { + if (supportedFlags.has(flag)) { + unsupportedFeatures.delete(flag); + } + } - if (supportScore >= priorityThreshold) { - selectedEngines.push({ engine, supportScore, unsupportedFeatures }); - meta.logger.debug(`Engine ${engine} meets feature priority threshold`, { supportScore, prioritySum, priorityThreshold, featureFlags: [...meta.featureFlags], unsupportedFeatures }); - } else { - meta.logger.debug(`Engine ${engine} does not meet feature priority threshold`, { supportScore, prioritySum, priorityThreshold, featureFlags: [...meta.featureFlags], unsupportedFeatures}); + if (supportScore >= priorityThreshold) { + selectedEngines.push({ engine, supportScore, unsupportedFeatures }); + meta.logger.debug(`Engine ${engine} meets feature priority threshold`, { + supportScore, + prioritySum, + priorityThreshold, + featureFlags: [...meta.featureFlags], + unsupportedFeatures + }); + } else { + meta.logger.debug( + `Engine ${engine} does not meet feature priority threshold`, + { + supportScore, + prioritySum, + priorityThreshold, + featureFlags: [...meta.featureFlags], + unsupportedFeatures } + ); } + } - if (selectedEngines.some(x => engineOptions[x.engine].quality > 0)) { - selectedEngines = selectedEngines.filter(x => engineOptions[x.engine].quality > 0); - } + if (selectedEngines.some((x) => engineOptions[x.engine].quality > 0)) { + selectedEngines = selectedEngines.filter( + (x) => engineOptions[x.engine].quality > 0 + ); + } - selectedEngines.sort((a,b) => b.supportScore - a.supportScore || engineOptions[b.engine].quality - engineOptions[a.engine].quality); + selectedEngines.sort( + (a, b) => + b.supportScore - a.supportScore || + engineOptions[b.engine].quality - engineOptions[a.engine].quality + ); - return selectedEngines; + return selectedEngines; } -export async function scrapeURLWithEngine(meta: Meta, engine: Engine): Promise { - const fn = engineHandlers[engine]; - const logger = meta.logger.child({ method: fn.name ?? "scrapeURLWithEngine", engine }); - const _meta = { - ...meta, - logger, - }; +export async function scrapeURLWithEngine( + meta: Meta, + engine: Engine +): Promise { + const fn = engineHandlers[engine]; + const logger = meta.logger.child({ + method: fn.name ?? "scrapeURLWithEngine", + engine + }); + const _meta = { + ...meta, + logger + }; - return await fn(_meta); + return await fn(_meta); } diff --git a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts index b441943c..62313a71 100644 --- a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts @@ -10,152 +10,179 @@ import PdfParse from "pdf-parse"; import { downloadFile, fetchFileToBuffer } from "../utils/downloadFile"; import { RemoveFeatureError } from "../../error"; -type PDFProcessorResult = {html: string, markdown?: string}; +type PDFProcessorResult = { html: string; markdown?: string }; -async function scrapePDFWithLlamaParse(meta: Meta, tempFilePath: string): Promise { - meta.logger.debug("Processing PDF document with LlamaIndex", { tempFilePath }); +async function scrapePDFWithLlamaParse( + meta: Meta, + tempFilePath: string +): Promise { + meta.logger.debug("Processing PDF document with LlamaIndex", { + tempFilePath + }); - const uploadForm = new FormData(); + const uploadForm = new FormData(); - // This is utterly stupid but it works! - mogery - uploadForm.append("file", { - [Symbol.toStringTag]: "Blob", - name: tempFilePath, - stream() { - return createReadStream(tempFilePath) as unknown as ReadableStream - }, - arrayBuffer() { - throw Error("Unimplemented in mock Blob: arrayBuffer") - }, - size: (await fs.stat(tempFilePath)).size, - text() { - throw Error("Unimplemented in mock Blob: text") - }, - slice(start, end, contentType) { - throw Error("Unimplemented in mock Blob: slice") - }, - type: "application/pdf", - } as Blob); + // This is utterly stupid but it works! - mogery + uploadForm.append("file", { + [Symbol.toStringTag]: "Blob", + name: tempFilePath, + stream() { + return createReadStream( + tempFilePath + ) as unknown as ReadableStream; + }, + arrayBuffer() { + throw Error("Unimplemented in mock Blob: arrayBuffer"); + }, + size: (await fs.stat(tempFilePath)).size, + text() { + throw Error("Unimplemented in mock Blob: text"); + }, + slice(start, end, contentType) { + throw Error("Unimplemented in mock Blob: slice"); + }, + type: "application/pdf" + } as Blob); - const upload = await robustFetch({ - url: "https://api.cloud.llamaindex.ai/api/parsing/upload", - method: "POST", + const upload = await robustFetch({ + url: "https://api.cloud.llamaindex.ai/api/parsing/upload", + method: "POST", + headers: { + Authorization: `Bearer ${process.env.LLAMAPARSE_API_KEY}` + }, + body: uploadForm, + logger: meta.logger.child({ + method: "scrapePDFWithLlamaParse/upload/robustFetch" + }), + schema: z.object({ + id: z.string() + }) + }); + + const jobId = upload.id; + + // TODO: timeout, retries + const startedAt = Date.now(); + + while (Date.now() <= startedAt + (meta.options.timeout ?? 300000)) { + try { + const result = await robustFetch({ + url: `https://api.cloud.llamaindex.ai/api/parsing/job/${jobId}/result/markdown`, + method: "GET", headers: { - "Authorization": `Bearer ${process.env.LLAMAPARSE_API_KEY}`, + Authorization: `Bearer ${process.env.LLAMAPARSE_API_KEY}` }, - body: uploadForm, - logger: meta.logger.child({ method: "scrapePDFWithLlamaParse/upload/robustFetch" }), - schema: z.object({ - id: z.string(), + logger: meta.logger.child({ + method: "scrapePDFWithLlamaParse/result/robustFetch" }), - }); - - const jobId = upload.id; - - // TODO: timeout, retries - const startedAt = Date.now(); - - while (Date.now() <= startedAt + (meta.options.timeout ?? 300000)) { - try { - const result = await robustFetch({ - url: `https://api.cloud.llamaindex.ai/api/parsing/job/${jobId}/result/markdown`, - method: "GET", - headers: { - "Authorization": `Bearer ${process.env.LLAMAPARSE_API_KEY}`, - }, - logger: meta.logger.child({ method: "scrapePDFWithLlamaParse/result/robustFetch" }), - schema: z.object({ - markdown: z.string(), - }), - }); - return { - markdown: result.markdown, - html: await marked.parse(result.markdown, { async: true }), - }; - } catch (e) { - if (e instanceof Error && e.message === "Request sent failure status") { - if ((e.cause as any).response.status === 404) { - // no-op, result not up yet - } else if ((e.cause as any).response.body.includes("PDF_IS_BROKEN")) { - // URL is not a PDF, actually! - meta.logger.debug("URL is not actually a PDF, signalling..."); - throw new RemoveFeatureError(["pdf"]); - } else { - throw new Error("LlamaParse threw an error", { - cause: e.cause, - }); - } - } else { - throw e; - } + schema: z.object({ + markdown: z.string() + }) + }); + return { + markdown: result.markdown, + html: await marked.parse(result.markdown, { async: true }) + }; + } catch (e) { + if (e instanceof Error && e.message === "Request sent failure status") { + if ((e.cause as any).response.status === 404) { + // no-op, result not up yet + } else if ((e.cause as any).response.body.includes("PDF_IS_BROKEN")) { + // URL is not a PDF, actually! + meta.logger.debug("URL is not actually a PDF, signalling..."); + throw new RemoveFeatureError(["pdf"]); + } else { + throw new Error("LlamaParse threw an error", { + cause: e.cause + }); } - - await new Promise((resolve) => setTimeout(() => resolve(), 250)); + } else { + throw e; + } } - throw new Error("LlamaParse timed out"); + await new Promise((resolve) => setTimeout(() => resolve(), 250)); + } + + throw new Error("LlamaParse timed out"); } -async function scrapePDFWithParsePDF(meta: Meta, tempFilePath: string): Promise { - meta.logger.debug("Processing PDF document with parse-pdf", { tempFilePath }); +async function scrapePDFWithParsePDF( + meta: Meta, + tempFilePath: string +): Promise { + meta.logger.debug("Processing PDF document with parse-pdf", { tempFilePath }); - const result = await PdfParse(await fs.readFile(tempFilePath)); - const escaped = escapeHtml(result.text); + const result = await PdfParse(await fs.readFile(tempFilePath)); + const escaped = escapeHtml(result.text); - return { - markdown: escaped, - html: escaped, - }; + return { + markdown: escaped, + html: escaped + }; } export async function scrapePDF(meta: Meta): Promise { - if (!meta.options.parsePDF) { - const file = await fetchFileToBuffer(meta.url); - const content = file.buffer.toString("base64"); - return { - url: file.response.url, - statusCode: file.response.status, - - html: content, - markdown: content, - }; - } - - const { response, tempFilePath } = await downloadFile(meta.id, meta.url); - - let result: PDFProcessorResult | null = null; - if (process.env.LLAMAPARSE_API_KEY) { - try { - result = await scrapePDFWithLlamaParse({ - ...meta, - logger: meta.logger.child({ method: "scrapePDF/scrapePDFWithLlamaParse" }), - }, tempFilePath); - } catch (error) { - if (error instanceof Error && error.message === "LlamaParse timed out") { - meta.logger.warn("LlamaParse timed out -- falling back to parse-pdf", { error }); - } else if (error instanceof RemoveFeatureError) { - throw error; - } else { - meta.logger.warn("LlamaParse failed to parse PDF -- falling back to parse-pdf", { error }); - Sentry.captureException(error); - } - } - } - - if (result === null) { - result = await scrapePDFWithParsePDF({ - ...meta, - logger: meta.logger.child({ method: "scrapePDF/scrapePDFWithParsePDF" }), - }, tempFilePath); - } - - await fs.unlink(tempFilePath); - + if (!meta.options.parsePDF) { + const file = await fetchFileToBuffer(meta.url); + const content = file.buffer.toString("base64"); return { - url: response.url, - statusCode: response.status, + url: file.response.url, + statusCode: file.response.status, - html: result.html, - markdown: result.markdown, + html: content, + markdown: content + }; + } + + const { response, tempFilePath } = await downloadFile(meta.id, meta.url); + + let result: PDFProcessorResult | null = null; + if (process.env.LLAMAPARSE_API_KEY) { + try { + result = await scrapePDFWithLlamaParse( + { + ...meta, + logger: meta.logger.child({ + method: "scrapePDF/scrapePDFWithLlamaParse" + }) + }, + tempFilePath + ); + } catch (error) { + if (error instanceof Error && error.message === "LlamaParse timed out") { + meta.logger.warn("LlamaParse timed out -- falling back to parse-pdf", { + error + }); + } else if (error instanceof RemoveFeatureError) { + throw error; + } else { + meta.logger.warn( + "LlamaParse failed to parse PDF -- falling back to parse-pdf", + { error } + ); + Sentry.captureException(error); + } } + } + + if (result === null) { + result = await scrapePDFWithParsePDF( + { + ...meta, + logger: meta.logger.child({ method: "scrapePDF/scrapePDFWithParsePDF" }) + }, + tempFilePath + ); + } + + await fs.unlink(tempFilePath); + + return { + url: response.url, + statusCode: response.status, + + html: result.html, + markdown: result.markdown + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts b/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts index 887b8b64..a8c16045 100644 --- a/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts @@ -4,39 +4,44 @@ import { Meta } from "../.."; import { TimeoutError } from "../../error"; import { robustFetch } from "../../lib/fetch"; -export async function scrapeURLWithPlaywright(meta: Meta): Promise { - const timeout = 20000 + meta.options.waitFor; +export async function scrapeURLWithPlaywright( + meta: Meta +): Promise { + const timeout = 20000 + meta.options.waitFor; - const response = await Promise.race([ - await robustFetch({ - url: process.env.PLAYWRIGHT_MICROSERVICE_URL!, - headers: { - "Content-Type": "application/json", - }, - body: { - url: meta.url, - wait_after_load: meta.options.waitFor, - timeout, - headers: meta.options.headers, - }, - method: "POST", - logger: meta.logger.child("scrapeURLWithPlaywright/robustFetch"), - schema: z.object({ - content: z.string(), - pageStatusCode: z.number(), - pageError: z.string().optional(), - }), - }), - (async () => { - await new Promise((resolve) => setTimeout(() => resolve(null), 20000)); - throw new TimeoutError("Playwright was unable to scrape the page before timing out", { cause: { timeout } }); - })(), - ]); + const response = await Promise.race([ + await robustFetch({ + url: process.env.PLAYWRIGHT_MICROSERVICE_URL!, + headers: { + "Content-Type": "application/json" + }, + body: { + url: meta.url, + wait_after_load: meta.options.waitFor, + timeout, + headers: meta.options.headers + }, + method: "POST", + logger: meta.logger.child("scrapeURLWithPlaywright/robustFetch"), + schema: z.object({ + content: z.string(), + pageStatusCode: z.number(), + pageError: z.string().optional() + }) + }), + (async () => { + await new Promise((resolve) => setTimeout(() => resolve(null), 20000)); + throw new TimeoutError( + "Playwright was unable to scrape the page before timing out", + { cause: { timeout } } + ); + })() + ]); - return { - url: meta.url, // TODO: impove redirect following - html: response.content, - statusCode: response.pageStatusCode, - error: response.pageError, - } + return { + url: meta.url, // TODO: impove redirect following + html: response.content, + statusCode: response.pageStatusCode, + error: response.pageError + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts b/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts index 9b946bf0..8388016a 100644 --- a/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts @@ -7,60 +7,82 @@ import { EngineError } from "../../error"; const client = new ScrapingBeeClient(process.env.SCRAPING_BEE_API_KEY!); -export function scrapeURLWithScrapingBee(wait_browser: "domcontentloaded" | "networkidle2"): ((meta: Meta) => Promise) { - return async (meta: Meta): Promise => { - let response: AxiosResponse; - try { - response = await client.get({ - url: meta.url, - params: { - timeout: 15000, // TODO: dynamic timeout based on request timeout - wait_browser: wait_browser, - wait: Math.min(meta.options.waitFor, 35000), - transparent_status_code: true, - json_response: true, - screenshot: meta.options.formats.includes("screenshot"), - screenshot_full_page: meta.options.formats.includes("screenshot@fullPage"), - }, - headers: { - "ScrapingService-Request": "TRUE", // this is sent to the page, not to ScrapingBee - mogery - }, - }); - } catch (error) { - if (error instanceof AxiosError && error.response !== undefined) { - response = error.response; - } else { - throw error; - } +export function scrapeURLWithScrapingBee( + wait_browser: "domcontentloaded" | "networkidle2" +): (meta: Meta) => Promise { + return async (meta: Meta): Promise => { + let response: AxiosResponse; + try { + response = await client.get({ + url: meta.url, + params: { + timeout: 15000, // TODO: dynamic timeout based on request timeout + wait_browser: wait_browser, + wait: Math.min(meta.options.waitFor, 35000), + transparent_status_code: true, + json_response: true, + screenshot: meta.options.formats.includes("screenshot"), + screenshot_full_page: meta.options.formats.includes( + "screenshot@fullPage" + ) + }, + headers: { + "ScrapingService-Request": "TRUE" // this is sent to the page, not to ScrapingBee - mogery } + }); + } catch (error) { + if (error instanceof AxiosError && error.response !== undefined) { + response = error.response; + } else { + throw error; + } + } - const data: Buffer = response.data; - const body = JSON.parse(new TextDecoder().decode(data)); + const data: Buffer = response.data; + const body = JSON.parse(new TextDecoder().decode(data)); - const headers = body.headers ?? {}; - const isHiddenEngineError = !(headers["Date"] ?? headers["date"] ?? headers["Content-Type"] ?? headers["content-type"]); + const headers = body.headers ?? {}; + const isHiddenEngineError = !( + headers["Date"] ?? + headers["date"] ?? + headers["Content-Type"] ?? + headers["content-type"] + ); - if (body.errors || body.body?.error || isHiddenEngineError) { - meta.logger.error("ScrapingBee threw an error", { body: body.body?.error ?? body.errors ?? body.body ?? body }); - throw new EngineError("Engine error #34", { cause: { body, statusCode: response.status } }); - } + if (body.errors || body.body?.error || isHiddenEngineError) { + meta.logger.error("ScrapingBee threw an error", { + body: body.body?.error ?? body.errors ?? body.body ?? body + }); + throw new EngineError("Engine error #34", { + cause: { body, statusCode: response.status } + }); + } - if (typeof body.body !== "string") { - meta.logger.error("ScrapingBee: Body is not string??", { body }); - throw new EngineError("Engine error #35", { cause: { body, statusCode: response.status } }); - } + if (typeof body.body !== "string") { + meta.logger.error("ScrapingBee: Body is not string??", { body }); + throw new EngineError("Engine error #35", { + cause: { body, statusCode: response.status } + }); + } - specialtyScrapeCheck(meta.logger.child({ method: "scrapeURLWithScrapingBee/specialtyScrapeCheck" }), body.headers); + specialtyScrapeCheck( + meta.logger.child({ + method: "scrapeURLWithScrapingBee/specialtyScrapeCheck" + }), + body.headers + ); - return { - url: body["resolved-url"] ?? meta.url, + return { + url: body["resolved-url"] ?? meta.url, - html: body.body, - error: response.status >= 300 ? response.statusText : undefined, - statusCode: response.status, - ...(body.screenshot ? ({ - screenshot: `data:image/png;base64,${body.screenshot}`, - }) : {}), - }; + html: body.body, + error: response.status >= 300 ? response.statusText : undefined, + statusCode: response.status, + ...(body.screenshot + ? { + screenshot: `data:image/png;base64,${body.screenshot}` + } + : {}) }; + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts b/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts index 736faba7..84a52425 100644 --- a/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts +++ b/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts @@ -7,48 +7,53 @@ import { v4 as uuid } from "uuid"; import * as undici from "undici"; export async function fetchFileToBuffer(url: string): Promise<{ - response: Response, - buffer: Buffer + response: Response; + buffer: Buffer; }> { - const response = await fetch(url); // TODO: maybe we could use tlsclient for this? for proxying - return { - response, - buffer: Buffer.from(await response.arrayBuffer()), - }; + const response = await fetch(url); // TODO: maybe we could use tlsclient for this? for proxying + return { + response, + buffer: Buffer.from(await response.arrayBuffer()) + }; } -export async function downloadFile(id: string, url: string): Promise<{ - response: undici.Response - tempFilePath: string +export async function downloadFile( + id: string, + url: string +): Promise<{ + response: undici.Response; + tempFilePath: string; }> { - const tempFilePath = path.join(os.tmpdir(), `tempFile-${id}--${uuid()}`); - const tempFileWrite = createWriteStream(tempFilePath); + const tempFilePath = path.join(os.tmpdir(), `tempFile-${id}--${uuid()}`); + const tempFileWrite = createWriteStream(tempFilePath); - // TODO: maybe we could use tlsclient for this? for proxying - // use undici to ignore SSL for now - const response = await undici.fetch(url, { - dispatcher: new undici.Agent({ - connect: { - rejectUnauthorized: false, - }, - }) - }); - - // This should never happen in the current state of JS (2024), but let's check anyways. - if (response.body === null) { - throw new EngineError("Response body was null", { cause: { response } }); - } - - response.body.pipeTo(Writable.toWeb(tempFileWrite)); - await new Promise((resolve, reject) => { - tempFileWrite.on("finish", () => resolve(null)); - tempFileWrite.on("error", (error) => { - reject(new EngineError("Failed to write to temp file", { cause: { error } })); - }); + // TODO: maybe we could use tlsclient for this? for proxying + // use undici to ignore SSL for now + const response = await undici.fetch(url, { + dispatcher: new undici.Agent({ + connect: { + rejectUnauthorized: false + } }) + }); - return { - response, - tempFilePath, - }; + // This should never happen in the current state of JS (2024), but let's check anyways. + if (response.body === null) { + throw new EngineError("Response body was null", { cause: { response } }); + } + + response.body.pipeTo(Writable.toWeb(tempFileWrite)); + await new Promise((resolve, reject) => { + tempFileWrite.on("finish", () => resolve(null)); + tempFileWrite.on("error", (error) => { + reject( + new EngineError("Failed to write to temp file", { cause: { error } }) + ); + }); + }); + + return { + response, + tempFilePath + }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts b/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts index b10657a4..4f497e52 100644 --- a/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts +++ b/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts @@ -1,14 +1,32 @@ import { Logger } from "winston"; import { AddFeatureError } from "../../error"; -export function specialtyScrapeCheck(logger: Logger, headers: Record | undefined) { - const contentType = (Object.entries(headers ?? {}).find(x => x[0].toLowerCase() === "content-type") ?? [])[1]; +export function specialtyScrapeCheck( + logger: Logger, + headers: Record | undefined +) { + const contentType = (Object.entries(headers ?? {}).find( + (x) => x[0].toLowerCase() === "content-type" + ) ?? [])[1]; - if (contentType === undefined) { - logger.warn("Failed to check contentType -- was not present in headers", { headers }); - } else if (contentType === "application/pdf" || contentType.startsWith("application/pdf;")) { // .pdf - throw new AddFeatureError(["pdf"]); - } else if (contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || contentType.startsWith("application/vnd.openxmlformats-officedocument.wordprocessingml.document;")) { // .docx - throw new AddFeatureError(["docx"]); - } + if (contentType === undefined) { + logger.warn("Failed to check contentType -- was not present in headers", { + headers + }); + } else if ( + contentType === "application/pdf" || + contentType.startsWith("application/pdf;") + ) { + // .pdf + throw new AddFeatureError(["pdf"]); + } else if ( + contentType === + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document;" + ) + ) { + // .docx + throw new AddFeatureError(["docx"]); + } } diff --git a/apps/api/src/scraper/scrapeURL/error.ts b/apps/api/src/scraper/scrapeURL/error.ts index ccd7a359..c6eb45e3 100644 --- a/apps/api/src/scraper/scrapeURL/error.ts +++ b/apps/api/src/scraper/scrapeURL/error.ts @@ -1,51 +1,57 @@ -import { EngineResultsTracker } from "." -import { Engine, FeatureFlag } from "./engines" +import { EngineResultsTracker } from "."; +import { Engine, FeatureFlag } from "./engines"; export class EngineError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options) - } + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + } } export class TimeoutError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options) - } + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + } } export class NoEnginesLeftError extends Error { - public fallbackList: Engine[]; - public results: EngineResultsTracker; + public fallbackList: Engine[]; + public results: EngineResultsTracker; - constructor(fallbackList: Engine[], results: EngineResultsTracker) { - super("All scraping engines failed! -- Double check the URL to make sure it's not broken. If the issue persists, contact us at help@firecrawl.com."); - this.fallbackList = fallbackList; - this.results = results; - } + constructor(fallbackList: Engine[], results: EngineResultsTracker) { + super( + "All scraping engines failed! -- Double check the URL to make sure it's not broken. If the issue persists, contact us at help@firecrawl.com." + ); + this.fallbackList = fallbackList; + this.results = results; + } } export class AddFeatureError extends Error { - public featureFlags: FeatureFlag[]; + public featureFlags: FeatureFlag[]; - constructor(featureFlags: FeatureFlag[]) { - super("New feature flags have been discovered: " + featureFlags.join(", ")); - this.featureFlags = featureFlags; - } + constructor(featureFlags: FeatureFlag[]) { + super("New feature flags have been discovered: " + featureFlags.join(", ")); + this.featureFlags = featureFlags; + } } export class RemoveFeatureError extends Error { - public featureFlags: FeatureFlag[]; + public featureFlags: FeatureFlag[]; - constructor(featureFlags: FeatureFlag[]) { - super("Incorrect feature flags have been discovered: " + featureFlags.join(", ")); - this.featureFlags = featureFlags; - } + constructor(featureFlags: FeatureFlag[]) { + super( + "Incorrect feature flags have been discovered: " + featureFlags.join(", ") + ); + this.featureFlags = featureFlags; + } } export class SiteError extends Error { - public code: string; - constructor(code: string) { - super("Specified URL is failing to load in the browser. Error code: " + code) - this.code = code; - } + public code: string; + constructor(code: string) { + super( + "Specified URL is failing to load in the browser. Error code: " + code + ); + this.code = code; + } } diff --git a/apps/api/src/scraper/scrapeURL/index.ts b/apps/api/src/scraper/scrapeURL/index.ts index f394ca2b..0a0b6c92 100644 --- a/apps/api/src/scraper/scrapeURL/index.ts +++ b/apps/api/src/scraper/scrapeURL/index.ts @@ -3,84 +3,104 @@ import * as Sentry from "@sentry/node"; import { Document, ScrapeOptions } from "../../controllers/v1/types"; import { logger } from "../../lib/logger"; -import { buildFallbackList, Engine, EngineScrapeResult, FeatureFlag, scrapeURLWithEngine } from "./engines"; +import { + buildFallbackList, + Engine, + EngineScrapeResult, + FeatureFlag, + scrapeURLWithEngine +} from "./engines"; import { parseMarkdown } from "../../lib/html-to-markdown"; -import { AddFeatureError, EngineError, NoEnginesLeftError, RemoveFeatureError, SiteError, TimeoutError } from "./error"; +import { + AddFeatureError, + EngineError, + NoEnginesLeftError, + RemoveFeatureError, + SiteError, + TimeoutError +} from "./error"; import { executeTransformers } from "./transformers"; import { LLMRefusalError } from "./transformers/llmExtract"; import { urlSpecificParams } from "./lib/urlSpecificParams"; -export type ScrapeUrlResponse = ({ - success: true, - document: Document, -} | { - success: false, - error: any, -}) & { - logs: any[], - engines: EngineResultsTracker, -} +export type ScrapeUrlResponse = ( + | { + success: true; + document: Document; + } + | { + success: false; + error: any; + } +) & { + logs: any[]; + engines: EngineResultsTracker; +}; export type Meta = { - id: string; - url: string; - options: ScrapeOptions; - internalOptions: InternalOptions; - logger: Logger; - logs: any[]; - featureFlags: Set; -} + id: string; + url: string; + options: ScrapeOptions; + internalOptions: InternalOptions; + logger: Logger; + logs: any[]; + featureFlags: Set; +}; -function buildFeatureFlags(url: string, options: ScrapeOptions, internalOptions: InternalOptions): Set { - const flags: Set = new Set(); +function buildFeatureFlags( + url: string, + options: ScrapeOptions, + internalOptions: InternalOptions +): Set { + const flags: Set = new Set(); - if (options.actions !== undefined) { - flags.add("actions"); - } + if (options.actions !== undefined) { + flags.add("actions"); + } - if (options.formats.includes("screenshot")) { - flags.add("screenshot"); - } + if (options.formats.includes("screenshot")) { + flags.add("screenshot"); + } - if (options.formats.includes("screenshot@fullPage")) { - flags.add("screenshot@fullScreen"); - } + if (options.formats.includes("screenshot@fullPage")) { + flags.add("screenshot@fullScreen"); + } - if (options.waitFor !== 0) { - flags.add("waitFor"); - } + if (options.waitFor !== 0) { + flags.add("waitFor"); + } - if (internalOptions.atsv) { - flags.add("atsv"); - } + if (internalOptions.atsv) { + flags.add("atsv"); + } - if (options.location || options.geolocation) { - flags.add("location"); - } + if (options.location || options.geolocation) { + flags.add("location"); + } - if (options.mobile) { - flags.add("mobile"); - } - - if (options.skipTlsVerification) { - flags.add("skipTlsVerification"); - } + if (options.mobile) { + flags.add("mobile"); + } - if (internalOptions.v0UseFastMode) { - flags.add("useFastMode"); - } + if (options.skipTlsVerification) { + flags.add("skipTlsVerification"); + } - const urlO = new URL(url); + if (internalOptions.v0UseFastMode) { + flags.add("useFastMode"); + } - if (urlO.pathname.endsWith(".pdf")) { - flags.add("pdf"); - } + const urlO = new URL(url); - if (urlO.pathname.endsWith(".docx")) { - flags.add("docx"); - } + if (urlO.pathname.endsWith(".pdf")) { + flags.add("pdf"); + } - return flags; + if (urlO.pathname.endsWith(".docx")) { + flags.add("docx"); + } + + return flags; } // The meta object contains all required information to perform a scrape. @@ -88,244 +108,314 @@ function buildFeatureFlags(url: string, options: ScrapeOptions, internalOptions: // The meta object is usually immutable, except for the logs array, and in edge cases (e.g. a new feature is suddenly required) // Having a meta object that is treated as immutable helps the code stay clean and easily tracable, // while also retaining the benefits that WebScraper had from its OOP design. -function buildMetaObject(id: string, url: string, options: ScrapeOptions, internalOptions: InternalOptions): Meta { - const specParams = urlSpecificParams[new URL(url).hostname.replace(/^www\./, "")]; - if (specParams !== undefined) { - options = Object.assign(options, specParams.scrapeOptions); - internalOptions = Object.assign(internalOptions, specParams.internalOptions); - } +function buildMetaObject( + id: string, + url: string, + options: ScrapeOptions, + internalOptions: InternalOptions +): Meta { + const specParams = + urlSpecificParams[new URL(url).hostname.replace(/^www\./, "")]; + if (specParams !== undefined) { + options = Object.assign(options, specParams.scrapeOptions); + internalOptions = Object.assign( + internalOptions, + specParams.internalOptions + ); + } - const _logger = logger.child({ module: "ScrapeURL", scrapeId: id, scrapeURL: url }); - const logs: any[] = []; + const _logger = logger.child({ + module: "ScrapeURL", + scrapeId: id, + scrapeURL: url + }); + const logs: any[] = []; - return { - id, url, options, internalOptions, - logger: _logger, - logs, - featureFlags: buildFeatureFlags(url, options, internalOptions), - }; + return { + id, + url, + options, + internalOptions, + logger: _logger, + logs, + featureFlags: buildFeatureFlags(url, options, internalOptions) + }; } export type InternalOptions = { - priority?: number; // Passed along to fire-engine - forceEngine?: Engine; - atsv?: boolean; // anti-bot solver, beta + priority?: number; // Passed along to fire-engine + forceEngine?: Engine; + atsv?: boolean; // anti-bot solver, beta - v0CrawlOnlyUrls?: boolean; - v0UseFastMode?: boolean; - v0DisableJsDom?: boolean; + v0CrawlOnlyUrls?: boolean; + v0UseFastMode?: boolean; + v0DisableJsDom?: boolean; - disableSmartWaitCache?: boolean; // Passed along to fire-engine + disableSmartWaitCache?: boolean; // Passed along to fire-engine }; -export type EngineResultsTracker = { [E in Engine]?: ({ - state: "error", - error: any, - unexpected: boolean, -} | { - state: "success", - result: EngineScrapeResult & { markdown: string }, - factors: Record, - unsupportedFeatures: Set, -} | { - state: "timeout", -}) & { - startedAt: number, - finishedAt: number, -} }; +export type EngineResultsTracker = { + [E in Engine]?: ( + | { + state: "error"; + error: any; + unexpected: boolean; + } + | { + state: "success"; + result: EngineScrapeResult & { markdown: string }; + factors: Record; + unsupportedFeatures: Set; + } + | { + state: "timeout"; + } + ) & { + startedAt: number; + finishedAt: number; + }; +}; export type EngineScrapeResultWithContext = { - engine: Engine, - unsupportedFeatures: Set, - result: (EngineScrapeResult & { markdown: string }), + engine: Engine; + unsupportedFeatures: Set; + result: EngineScrapeResult & { markdown: string }; }; function safeguardCircularError(error: T): T { - if (typeof error === "object" && error !== null && (error as any).results) { - const newError = structuredClone(error); - delete (newError as any).results; - return newError; - } else { - return error; - } + if (typeof error === "object" && error !== null && (error as any).results) { + const newError = structuredClone(error); + delete (newError as any).results; + return newError; + } else { + return error; + } } -async function scrapeURLLoop( - meta: Meta -): Promise { - meta.logger.info(`Scraping URL ${JSON.stringify(meta.url)}...`,); +async function scrapeURLLoop(meta: Meta): Promise { + meta.logger.info(`Scraping URL ${JSON.stringify(meta.url)}...`); - // TODO: handle sitemap data, see WebScraper/index.ts:280 - // TODO: ScrapeEvents + // TODO: handle sitemap data, see WebScraper/index.ts:280 + // TODO: ScrapeEvents - const fallbackList = buildFallbackList(meta); + const fallbackList = buildFallbackList(meta); - const results: EngineResultsTracker = {}; - let result: EngineScrapeResultWithContext | null = null; + const results: EngineResultsTracker = {}; + let result: EngineScrapeResultWithContext | null = null; - for (const { engine, unsupportedFeatures } of fallbackList) { - const startedAt = Date.now(); - try { - meta.logger.info("Scraping via " + engine + "..."); - const _engineResult = await scrapeURLWithEngine(meta, engine); - if (_engineResult.markdown === undefined) { // Some engines emit Markdown directly. - _engineResult.markdown = await parseMarkdown(_engineResult.html); - } - const engineResult = _engineResult as EngineScrapeResult & { markdown: string }; + for (const { engine, unsupportedFeatures } of fallbackList) { + const startedAt = Date.now(); + try { + meta.logger.info("Scraping via " + engine + "..."); + const _engineResult = await scrapeURLWithEngine(meta, engine); + if (_engineResult.markdown === undefined) { + // Some engines emit Markdown directly. + _engineResult.markdown = await parseMarkdown(_engineResult.html); + } + const engineResult = _engineResult as EngineScrapeResult & { + markdown: string; + }; - // Success factors - const isLongEnough = engineResult.markdown.length >= 20; - const isGoodStatusCode = (engineResult.statusCode >= 200 && engineResult.statusCode < 300) || engineResult.statusCode === 304; - const hasNoPageError = engineResult.error === undefined; + // Success factors + const isLongEnough = engineResult.markdown.length >= 20; + const isGoodStatusCode = + (engineResult.statusCode >= 200 && engineResult.statusCode < 300) || + engineResult.statusCode === 304; + const hasNoPageError = engineResult.error === undefined; - results[engine] = { - state: "success", - result: engineResult, - factors: { isLongEnough, isGoodStatusCode, hasNoPageError }, - unsupportedFeatures, - startedAt, - finishedAt: Date.now(), - }; + results[engine] = { + state: "success", + result: engineResult, + factors: { isLongEnough, isGoodStatusCode, hasNoPageError }, + unsupportedFeatures, + startedAt, + finishedAt: Date.now() + }; - // NOTE: TODO: what to do when status code is bad is tough... - // we cannot just rely on text because error messages can be brief and not hit the limit - // should we just use all the fallbacks and pick the one with the longest text? - mogery - if (isLongEnough || !isGoodStatusCode) { - meta.logger.info("Scrape via " + engine + " deemed successful.", { factors: { isLongEnough, isGoodStatusCode, hasNoPageError } }); - result = { - engine, - unsupportedFeatures, - result: engineResult as EngineScrapeResult & { markdown: string } - }; - break; - } - } catch (error) { - if (error instanceof EngineError) { - meta.logger.info("Engine " + engine + " could not scrape the page.", { error }); - results[engine] = { - state: "error", - error: safeguardCircularError(error), - unexpected: false, - startedAt, - finishedAt: Date.now(), - }; - } else if (error instanceof TimeoutError) { - meta.logger.info("Engine " + engine + " timed out while scraping.", { error }); - results[engine] = { - state: "timeout", - startedAt, - finishedAt: Date.now(), - }; - } else if (error instanceof AddFeatureError || error instanceof RemoveFeatureError) { - throw error; - } else if (error instanceof LLMRefusalError) { - results[engine] = { - state: "error", - error: safeguardCircularError(error), - unexpected: true, - startedAt, - finishedAt: Date.now(), - } - error.results = results; - meta.logger.warn("LLM refusal encountered", { error }); - throw error; - } else if (error instanceof SiteError) { - throw error; - } else { - Sentry.captureException(error); - meta.logger.info("An unexpected error happened while scraping with " + engine + ".", { error }); - results[engine] = { - state: "error", - error: safeguardCircularError(error), - unexpected: true, - startedAt, - finishedAt: Date.now(), - } - } - } + // NOTE: TODO: what to do when status code is bad is tough... + // we cannot just rely on text because error messages can be brief and not hit the limit + // should we just use all the fallbacks and pick the one with the longest text? - mogery + if (isLongEnough || !isGoodStatusCode) { + meta.logger.info("Scrape via " + engine + " deemed successful.", { + factors: { isLongEnough, isGoodStatusCode, hasNoPageError } + }); + result = { + engine, + unsupportedFeatures, + result: engineResult as EngineScrapeResult & { markdown: string } + }; + break; + } + } catch (error) { + if (error instanceof EngineError) { + meta.logger.info("Engine " + engine + " could not scrape the page.", { + error + }); + results[engine] = { + state: "error", + error: safeguardCircularError(error), + unexpected: false, + startedAt, + finishedAt: Date.now() + }; + } else if (error instanceof TimeoutError) { + meta.logger.info("Engine " + engine + " timed out while scraping.", { + error + }); + results[engine] = { + state: "timeout", + startedAt, + finishedAt: Date.now() + }; + } else if ( + error instanceof AddFeatureError || + error instanceof RemoveFeatureError + ) { + throw error; + } else if (error instanceof LLMRefusalError) { + results[engine] = { + state: "error", + error: safeguardCircularError(error), + unexpected: true, + startedAt, + finishedAt: Date.now() + }; + error.results = results; + meta.logger.warn("LLM refusal encountered", { error }); + throw error; + } else if (error instanceof SiteError) { + throw error; + } else { + Sentry.captureException(error); + meta.logger.info( + "An unexpected error happened while scraping with " + engine + ".", + { error } + ); + results[engine] = { + state: "error", + error: safeguardCircularError(error), + unexpected: true, + startedAt, + finishedAt: Date.now() + }; + } } + } - if (result === null) { - throw new NoEnginesLeftError(fallbackList.map(x => x.engine), results); + if (result === null) { + throw new NoEnginesLeftError( + fallbackList.map((x) => x.engine), + results + ); + } + + let document: Document = { + markdown: result.result.markdown, + rawHtml: result.result.html, + screenshot: result.result.screenshot, + actions: result.result.actions, + metadata: { + sourceURL: meta.url, + url: result.result.url, + statusCode: result.result.statusCode, + error: result.result.error } + }; - let document: Document = { - markdown: result.result.markdown, - rawHtml: result.result.html, - screenshot: result.result.screenshot, - actions: result.result.actions, - metadata: { - sourceURL: meta.url, - url: result.result.url, - statusCode: result.result.statusCode, - error: result.result.error, - }, - } + if (result.unsupportedFeatures.size > 0) { + const warning = `The engine used does not support the following features: ${[...result.unsupportedFeatures].join(", ")} -- your scrape may be partial.`; + meta.logger.warn(warning, { + engine: result.engine, + unsupportedFeatures: result.unsupportedFeatures + }); + document.warning = + document.warning !== undefined + ? document.warning + " " + warning + : warning; + } - if (result.unsupportedFeatures.size > 0) { - const warning = `The engine used does not support the following features: ${[...result.unsupportedFeatures].join(", ")} -- your scrape may be partial.`; - meta.logger.warn(warning, { engine: result.engine, unsupportedFeatures: result.unsupportedFeatures }); - document.warning = document.warning !== undefined ? document.warning + " " + warning : warning; - } + document = await executeTransformers(meta, document); - document = await executeTransformers(meta, document); - - return { - success: true, - document, - logs: meta.logs, - engines: results, - }; + return { + success: true, + document, + logs: meta.logs, + engines: results + }; } export async function scrapeURL( - id: string, - url: string, - options: ScrapeOptions, - internalOptions: InternalOptions = {}, + id: string, + url: string, + options: ScrapeOptions, + internalOptions: InternalOptions = {} ): Promise { - const meta = buildMetaObject(id, url, options, internalOptions); - try { - while (true) { - try { - return await scrapeURLLoop(meta); - } catch (error) { - if (error instanceof AddFeatureError && meta.internalOptions.forceEngine === undefined) { - meta.logger.debug("More feature flags requested by scraper: adding " + error.featureFlags.join(", "), { error, existingFlags: meta.featureFlags }); - meta.featureFlags = new Set([...meta.featureFlags].concat(error.featureFlags)); - } else if (error instanceof RemoveFeatureError && meta.internalOptions.forceEngine === undefined) { - meta.logger.debug("Incorrect feature flags reported by scraper: removing " + error.featureFlags.join(","), { error, existingFlags: meta.featureFlags }); - meta.featureFlags = new Set([...meta.featureFlags].filter(x => !error.featureFlags.includes(x))); - } else { - throw error; - } - } - } - } catch (error) { - let results: EngineResultsTracker = {}; - - if (error instanceof NoEnginesLeftError) { - meta.logger.warn("scrapeURL: All scraping engines failed!", { error }); - results = error.results; - } else if (error instanceof LLMRefusalError) { - meta.logger.warn("scrapeURL: LLM refused to extract content", { error }); - results = error.results!; - } else if (error instanceof Error && error.message.includes("Invalid schema for response_format")) { // TODO: seperate into custom error - meta.logger.warn("scrapeURL: LLM schema error", { error }); - // TODO: results? - } else if (error instanceof SiteError) { - meta.logger.warn("scrapeURL: Site failed to load in browser", { error }); + const meta = buildMetaObject(id, url, options, internalOptions); + try { + while (true) { + try { + return await scrapeURLLoop(meta); + } catch (error) { + if ( + error instanceof AddFeatureError && + meta.internalOptions.forceEngine === undefined + ) { + meta.logger.debug( + "More feature flags requested by scraper: adding " + + error.featureFlags.join(", "), + { error, existingFlags: meta.featureFlags } + ); + meta.featureFlags = new Set( + [...meta.featureFlags].concat(error.featureFlags) + ); + } else if ( + error instanceof RemoveFeatureError && + meta.internalOptions.forceEngine === undefined + ) { + meta.logger.debug( + "Incorrect feature flags reported by scraper: removing " + + error.featureFlags.join(","), + { error, existingFlags: meta.featureFlags } + ); + meta.featureFlags = new Set( + [...meta.featureFlags].filter( + (x) => !error.featureFlags.includes(x) + ) + ); } else { - Sentry.captureException(error); - meta.logger.error("scrapeURL: Unexpected error happened", { error }); - // TODO: results? - } - - return { - success: false, - error, - logs: meta.logs, - engines: results, + throw error; } + } } + } catch (error) { + let results: EngineResultsTracker = {}; + + if (error instanceof NoEnginesLeftError) { + meta.logger.warn("scrapeURL: All scraping engines failed!", { error }); + results = error.results; + } else if (error instanceof LLMRefusalError) { + meta.logger.warn("scrapeURL: LLM refused to extract content", { error }); + results = error.results!; + } else if ( + error instanceof Error && + error.message.includes("Invalid schema for response_format") + ) { + // TODO: seperate into custom error + meta.logger.warn("scrapeURL: LLM schema error", { error }); + // TODO: results? + } else if (error instanceof SiteError) { + meta.logger.warn("scrapeURL: Site failed to load in browser", { error }); + } else { + Sentry.captureException(error); + meta.logger.error("scrapeURL: Unexpected error happened", { error }); + // TODO: results? + } + + return { + success: false, + error, + logs: meta.logs, + engines: results + }; + } } diff --git a/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts b/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts index 484bf725..6d71c036 100644 --- a/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts +++ b/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts @@ -3,33 +3,36 @@ import { load } from "cheerio"; import { logger } from "../../../lib/logger"; export function extractLinks(html: string, baseUrl: string): string[] { - const $ = load(html); - const links: string[] = []; - - $('a').each((_, element) => { - const href = $(element).attr('href'); - if (href) { - try { - if (href.startsWith('http://') || href.startsWith('https://')) { - // Absolute URL, add as is - links.push(href); - } else if (href.startsWith('/')) { - // Relative URL starting with '/', append to origin - links.push(new URL(href, baseUrl).href); - } else if (!href.startsWith('#') && !href.startsWith('mailto:')) { - // Relative URL not starting with '/', append to base URL - links.push(new URL(href, baseUrl).href); - } else if (href.startsWith('mailto:')) { - // mailto: links, add as is - links.push(href); - } - // Fragment-only links (#) are ignored - } catch (error) { - logger.error(`Failed to construct URL for href: ${href} with base: ${baseUrl}`, { error }); - } + const $ = load(html); + const links: string[] = []; + + $("a").each((_, element) => { + const href = $(element).attr("href"); + if (href) { + try { + if (href.startsWith("http://") || href.startsWith("https://")) { + // Absolute URL, add as is + links.push(href); + } else if (href.startsWith("/")) { + // Relative URL starting with '/', append to origin + links.push(new URL(href, baseUrl).href); + } else if (!href.startsWith("#") && !href.startsWith("mailto:")) { + // Relative URL not starting with '/', append to base URL + links.push(new URL(href, baseUrl).href); + } else if (href.startsWith("mailto:")) { + // mailto: links, add as is + links.push(href); } - }); - - // Remove duplicates and return - return [...new Set(links)]; -} \ No newline at end of file + // Fragment-only links (#) are ignored + } catch (error) { + logger.error( + `Failed to construct URL for href: ${href} with base: ${baseUrl}`, + { error } + ); + } + } + }); + + // Remove duplicates and return + return [...new Set(links)]; +} diff --git a/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts b/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts index f3fe5a5b..0f581373 100644 --- a/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts +++ b/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts @@ -2,7 +2,10 @@ import { load } from "cheerio"; import { Document } from "../../../controllers/v1/types"; import { Meta } from ".."; -export function extractMetadata(meta: Meta, html: string): Document["metadata"] { +export function extractMetadata( + meta: Meta, + html: string +): Document["metadata"] { let title: string | undefined = undefined; let description: string | undefined = undefined; let language: string | undefined = undefined; @@ -39,36 +42,54 @@ export function extractMetadata(meta: Meta, html: string): Document["metadata"] try { title = soup("title").text() || undefined; description = soup('meta[name="description"]').attr("content") || undefined; - + // Assuming the language is part of the URL as per the regex pattern - language = soup('html').attr('lang') || undefined; + language = soup("html").attr("lang") || undefined; keywords = soup('meta[name="keywords"]').attr("content") || undefined; robots = soup('meta[name="robots"]').attr("content") || undefined; ogTitle = soup('meta[property="og:title"]').attr("content") || undefined; - ogDescription = soup('meta[property="og:description"]').attr("content") || undefined; + ogDescription = + soup('meta[property="og:description"]').attr("content") || undefined; ogUrl = soup('meta[property="og:url"]').attr("content") || undefined; ogImage = soup('meta[property="og:image"]').attr("content") || undefined; ogAudio = soup('meta[property="og:audio"]').attr("content") || undefined; - ogDeterminer = soup('meta[property="og:determiner"]').attr("content") || undefined; + ogDeterminer = + soup('meta[property="og:determiner"]').attr("content") || undefined; ogLocale = soup('meta[property="og:locale"]').attr("content") || undefined; - ogLocaleAlternate = soup('meta[property="og:locale:alternate"]').map((i, el) => soup(el).attr("content")).get() || undefined; - ogSiteName = soup('meta[property="og:site_name"]').attr("content") || undefined; + ogLocaleAlternate = + soup('meta[property="og:locale:alternate"]') + .map((i, el) => soup(el).attr("content")) + .get() || undefined; + ogSiteName = + soup('meta[property="og:site_name"]').attr("content") || undefined; ogVideo = soup('meta[property="og:video"]').attr("content") || undefined; - articleSection = soup('meta[name="article:section"]').attr("content") || undefined; + articleSection = + soup('meta[name="article:section"]').attr("content") || undefined; articleTag = soup('meta[name="article:tag"]').attr("content") || undefined; - publishedTime = soup('meta[property="article:published_time"]').attr("content") || undefined; - modifiedTime = soup('meta[property="article:modified_time"]').attr("content") || undefined; - dcTermsKeywords = soup('meta[name="dcterms.keywords"]').attr("content") || undefined; - dcDescription = soup('meta[name="dc.description"]').attr("content") || undefined; + publishedTime = + soup('meta[property="article:published_time"]').attr("content") || + undefined; + modifiedTime = + soup('meta[property="article:modified_time"]').attr("content") || + undefined; + dcTermsKeywords = + soup('meta[name="dcterms.keywords"]').attr("content") || undefined; + dcDescription = + soup('meta[name="dc.description"]').attr("content") || undefined; dcSubject = soup('meta[name="dc.subject"]').attr("content") || undefined; - dcTermsSubject = soup('meta[name="dcterms.subject"]').attr("content") || undefined; - dcTermsAudience = soup('meta[name="dcterms.audience"]').attr("content") || undefined; + dcTermsSubject = + soup('meta[name="dcterms.subject"]').attr("content") || undefined; + dcTermsAudience = + soup('meta[name="dcterms.audience"]').attr("content") || undefined; dcType = soup('meta[name="dc.type"]').attr("content") || undefined; - dcTermsType = soup('meta[name="dcterms.type"]').attr("content") || undefined; + dcTermsType = + soup('meta[name="dcterms.type"]').attr("content") || undefined; dcDate = soup('meta[name="dc.date"]').attr("content") || undefined; - dcDateCreated = soup('meta[name="dc.date.created"]').attr("content") || undefined; - dcTermsCreated = soup('meta[name="dcterms.created"]').attr("content") || undefined; + dcDateCreated = + soup('meta[name="dc.date.created"]').attr("content") || undefined; + dcTermsCreated = + soup('meta[name="dcterms.created"]').attr("content") || undefined; try { // Extract all meta tags for custom metadata @@ -127,6 +148,6 @@ export function extractMetadata(meta: Meta, html: string): Document["metadata"] publishedTime, articleTag, articleSection, - ...customMetadata, + ...customMetadata }; } diff --git a/apps/api/src/scraper/scrapeURL/lib/fetch.ts b/apps/api/src/scraper/scrapeURL/lib/fetch.ts index 09a280b8..400c23a7 100644 --- a/apps/api/src/scraper/scrapeURL/lib/fetch.ts +++ b/apps/api/src/scraper/scrapeURL/lib/fetch.ts @@ -4,143 +4,210 @@ import { v4 as uuid } from "uuid"; import * as Sentry from "@sentry/node"; export type RobustFetchParams> = { - url: string; - logger: Logger, - method: "GET" | "POST" | "DELETE" | "PUT"; - body?: any; - headers?: Record; - schema?: Schema; - dontParseResponse?: boolean; - ignoreResponse?: boolean; - ignoreFailure?: boolean; - requestId?: string; - tryCount?: number; - tryCooldown?: number; + url: string; + logger: Logger; + method: "GET" | "POST" | "DELETE" | "PUT"; + body?: any; + headers?: Record; + schema?: Schema; + dontParseResponse?: boolean; + ignoreResponse?: boolean; + ignoreFailure?: boolean; + requestId?: string; + tryCount?: number; + tryCooldown?: number; }; -export async function robustFetch, Output = z.infer>({ +export async function robustFetch< + Schema extends z.Schema, + Output = z.infer +>({ + url, + logger, + method = "GET", + body, + headers, + schema, + ignoreResponse = false, + ignoreFailure = false, + requestId = uuid(), + tryCount = 1, + tryCooldown +}: RobustFetchParams): Promise { + const params = { url, logger, - method = "GET", + method, body, headers, schema, - ignoreResponse = false, - ignoreFailure = false, - requestId = uuid(), - tryCount = 1, - tryCooldown, -}: RobustFetchParams): Promise { - const params = { url, logger, method, body, headers, schema, ignoreResponse, ignoreFailure, tryCount, tryCooldown }; + ignoreResponse, + ignoreFailure, + tryCount, + tryCooldown + }; - let request: Response; - try { - request = await fetch(url, { - method, - headers: { - ...(body instanceof FormData - ? ({}) - : body !== undefined ? ({ - "Content-Type": "application/json", - }) : {}), - ...(headers !== undefined ? headers : {}), - }, - ...(body instanceof FormData ? ({ - body, - }) : body !== undefined ? ({ - body: JSON.stringify(body), - }) : {}), + let request: Response; + try { + request = await fetch(url, { + method, + headers: { + ...(body instanceof FormData + ? {} + : body !== undefined + ? { + "Content-Type": "application/json" + } + : {}), + ...(headers !== undefined ? headers : {}) + }, + ...(body instanceof FormData + ? { + body + } + : body !== undefined + ? { + body: JSON.stringify(body) + } + : {}) + }); + } catch (error) { + if (!ignoreFailure) { + Sentry.captureException(error); + if (tryCount > 1) { + logger.debug( + "Request failed, trying " + (tryCount - 1) + " more times", + { params, error, requestId } + ); + return await robustFetch({ + ...params, + requestId, + tryCount: tryCount - 1 }); - } catch (error) { - if (!ignoreFailure) { - Sentry.captureException(error); - if (tryCount > 1) { - logger.debug("Request failed, trying " + (tryCount - 1) + " more times", { params, error, requestId }); - return await robustFetch({ - ...params, - requestId, - tryCount: tryCount - 1, - }); - } else { - logger.debug("Request failed", { params, error, requestId }); - throw new Error("Request failed", { - cause: { - params, requestId, error, - }, - }); - } - - } else { - return null as Output; - } - } - - if (ignoreResponse === true) { - return null as Output; - } - - const response = { - status: request.status, - headers: request.headers, - body: await request.text(), // NOTE: can this throw an exception? - }; - - if (request.status >= 300) { - if (tryCount > 1) { - logger.debug("Request sent failure status, trying " + (tryCount - 1) + " more times", { params, request, response, requestId }); - if (tryCooldown !== undefined) { - await new Promise((resolve) => setTimeout(() => resolve(null), tryCooldown)); - } - return await robustFetch({ - ...params, - requestId, - tryCount: tryCount - 1, - }); - } else { - logger.debug("Request sent failure status", { params, request, response, requestId }); - throw new Error("Request sent failure status", { - cause: { - params, request, response, requestId, - }, - }); - } - } - - let data: Output; - try { - data = JSON.parse(response.body); - } catch (error) { - logger.debug("Request sent malformed JSON", { params, request, response, requestId }); - throw new Error("Request sent malformed JSON", { - cause: { - params, request, response, requestId, - }, + } else { + logger.debug("Request failed", { params, error, requestId }); + throw new Error("Request failed", { + cause: { + params, + requestId, + error + } }); + } + } else { + return null as Output; } + } - if (schema) { - try { - data = schema.parse(data); - } catch (error) { - if (error instanceof ZodError) { - logger.debug("Response does not match provided schema", { params, request, response, requestId, error, schema }); - throw new Error("Response does not match provided schema", { - cause: { - params, request, response, requestId, - error, schema, - } - }); - } else { - logger.debug("Parsing response with provided schema failed", { params, request, response, requestId, error, schema }); - throw new Error("Parsing response with provided schema failed", { - cause: { - params, request, response, requestId, - error, schema - } - }); - } + if (ignoreResponse === true) { + return null as Output; + } + + const response = { + status: request.status, + headers: request.headers, + body: await request.text() // NOTE: can this throw an exception? + }; + + if (request.status >= 300) { + if (tryCount > 1) { + logger.debug( + "Request sent failure status, trying " + (tryCount - 1) + " more times", + { params, request, response, requestId } + ); + if (tryCooldown !== undefined) { + await new Promise((resolve) => + setTimeout(() => resolve(null), tryCooldown) + ); + } + return await robustFetch({ + ...params, + requestId, + tryCount: tryCount - 1 + }); + } else { + logger.debug("Request sent failure status", { + params, + request, + response, + requestId + }); + throw new Error("Request sent failure status", { + cause: { + params, + request, + response, + requestId } + }); } + } - return data; -} \ No newline at end of file + let data: Output; + try { + data = JSON.parse(response.body); + } catch (error) { + logger.debug("Request sent malformed JSON", { + params, + request, + response, + requestId + }); + throw new Error("Request sent malformed JSON", { + cause: { + params, + request, + response, + requestId + } + }); + } + + if (schema) { + try { + data = schema.parse(data); + } catch (error) { + if (error instanceof ZodError) { + logger.debug("Response does not match provided schema", { + params, + request, + response, + requestId, + error, + schema + }); + throw new Error("Response does not match provided schema", { + cause: { + params, + request, + response, + requestId, + error, + schema + } + }); + } else { + logger.debug("Parsing response with provided schema failed", { + params, + request, + response, + requestId, + error, + schema + }); + throw new Error("Parsing response with provided schema failed", { + cause: { + params, + request, + response, + requestId, + error, + schema + } + }); + } + } + } + + return data; +} diff --git a/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts b/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts index 9458ed0f..7701aeaf 100644 --- a/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts +++ b/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts @@ -4,55 +4,53 @@ import { AnyNode, Cheerio, load } from "cheerio"; import { ScrapeOptions } from "../../../controllers/v1/types"; const excludeNonMainTags = [ - "header", - "footer", - "nav", - "aside", - ".header", - ".top", - ".navbar", - "#header", - ".footer", - ".bottom", - "#footer", - ".sidebar", - ".side", - ".aside", - "#sidebar", - ".modal", - ".popup", - "#modal", - ".overlay", - ".ad", - ".ads", - ".advert", - "#ad", - ".lang-selector", - ".language", - "#language-selector", - ".social", - ".social-media", - ".social-links", - "#social", - ".menu", - ".navigation", - "#nav", - ".breadcrumbs", - "#breadcrumbs", - "#search-form", - ".search", - "#search", - ".share", - "#share", - ".widget", - "#widget", - ".cookie", - "#cookie" + "header", + "footer", + "nav", + "aside", + ".header", + ".top", + ".navbar", + "#header", + ".footer", + ".bottom", + "#footer", + ".sidebar", + ".side", + ".aside", + "#sidebar", + ".modal", + ".popup", + "#modal", + ".overlay", + ".ad", + ".ads", + ".advert", + "#ad", + ".lang-selector", + ".language", + "#language-selector", + ".social", + ".social-media", + ".social-links", + "#social", + ".menu", + ".navigation", + "#nav", + ".breadcrumbs", + "#breadcrumbs", + "#search-form", + ".search", + "#search", + ".share", + "#share", + ".widget", + "#widget", + ".cookie", + "#cookie" ]; -const forceIncludeMainTags = [ - "#main" -]; +const forceIncludeMainTags = ["#main"]; export const removeUnwantedElements = ( html: string, @@ -60,58 +58,65 @@ export const removeUnwantedElements = ( ) => { const soup = load(html); - if (scrapeOptions.includeTags && scrapeOptions.includeTags.filter(x => x.trim().length !== 0).length > 0) { + if ( + scrapeOptions.includeTags && + scrapeOptions.includeTags.filter((x) => x.trim().length !== 0).length > 0 + ) { // Create a new root element to hold the tags to keep const newRoot = load("
")("div"); scrapeOptions.includeTags.forEach((tag) => { - soup(tag).each((_, element) => { - newRoot.append(soup(element).clone()); - }); + soup(tag).each((_, element) => { + newRoot.append(soup(element).clone()); + }); }); return newRoot.html() ?? ""; } soup("script, style, noscript, meta, head").remove(); - if (scrapeOptions.excludeTags && scrapeOptions.excludeTags.filter(x => x.trim().length !== 0).length > 0) { - scrapeOptions.excludeTags.forEach((tag) => { - let elementsToRemove: Cheerio; - if (tag.startsWith("*") && tag.endsWith("*")) { - let classMatch = false; + if ( + scrapeOptions.excludeTags && + scrapeOptions.excludeTags.filter((x) => x.trim().length !== 0).length > 0 + ) { + scrapeOptions.excludeTags.forEach((tag) => { + let elementsToRemove: Cheerio; + if (tag.startsWith("*") && tag.endsWith("*")) { + let classMatch = false; - const regexPattern = new RegExp(tag.slice(1, -1), "i"); - elementsToRemove = soup("*").filter((i, element) => { - if (element.type === "tag") { - const attributes = element.attribs; - const tagNameMatches = regexPattern.test(element.name); - const attributesMatch = Object.keys(attributes).some((attr) => - regexPattern.test(`${attr}="${attributes[attr]}"`) - ); - if (tag.startsWith("*.")) { - classMatch = Object.keys(attributes).some((attr) => - regexPattern.test(`class="${attributes[attr]}"`) - ); - } - return tagNameMatches || attributesMatch || classMatch; - } - return false; - }); - } else { - elementsToRemove = soup(tag); + const regexPattern = new RegExp(tag.slice(1, -1), "i"); + elementsToRemove = soup("*").filter((i, element) => { + if (element.type === "tag") { + const attributes = element.attribs; + const tagNameMatches = regexPattern.test(element.name); + const attributesMatch = Object.keys(attributes).some((attr) => + regexPattern.test(`${attr}="${attributes[attr]}"`) + ); + if (tag.startsWith("*.")) { + classMatch = Object.keys(attributes).some((attr) => + regexPattern.test(`class="${attributes[attr]}"`) + ); } - elementsToRemove.remove(); + return tagNameMatches || attributesMatch || classMatch; + } + return false; }); - } + } else { + elementsToRemove = soup(tag); + } + elementsToRemove.remove(); + }); + } - if (scrapeOptions.onlyMainContent) { - excludeNonMainTags.forEach((tag) => { - const elementsToRemove = soup(tag) - .filter(forceIncludeMainTags.map(x => ":not(:has(" + x + "))").join("")); - - elementsToRemove.remove(); - }); - } - - const cleanedHtml = soup.html(); - return cleanedHtml; + if (scrapeOptions.onlyMainContent) { + excludeNonMainTags.forEach((tag) => { + const elementsToRemove = soup(tag).filter( + forceIncludeMainTags.map((x) => ":not(:has(" + x + "))").join("") + ); + + elementsToRemove.remove(); + }); + } + + const cleanedHtml = soup.html(); + return cleanedHtml; }; diff --git a/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts b/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts index 7ce4f66e..0810dc93 100644 --- a/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts +++ b/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts @@ -2,8 +2,8 @@ import { InternalOptions } from ".."; import { ScrapeOptions } from "../../../controllers/v1/types"; export type UrlSpecificParams = { - scrapeOptions: Partial, - internalOptions: Partial, + scrapeOptions: Partial; + internalOptions: Partial; }; // const docsParam: UrlSpecificParams = { @@ -12,40 +12,40 @@ export type UrlSpecificParams = { // } export const urlSpecificParams: Record = { - // "support.greenpay.me": docsParam, - // "docs.pdw.co": docsParam, - // "developers.notion.com": docsParam, - // "docs2.hubitat.com": docsParam, - // "rsseau.fr": docsParam, - // "help.salesforce.com": docsParam, - // "scrapethissite.com": { - // scrapeOptions: {}, - // internalOptions: { forceEngine: "fetch" }, - // }, - // "eonhealth.com": { - // defaultScraper: "fire-engine", - // params: { - // fireEngineOptions: { - // mobileProxy: true, - // method: "get", - // engine: "request", - // }, - // }, - // }, - // "notion.com": { - // scrapeOptions: { waitFor: 2000 }, - // internalOptions: { forceEngine: "fire-engine;playwright" } - // }, - // "developer.apple.com": { - // scrapeOptions: { waitFor: 2000 }, - // internalOptions: { forceEngine: "fire-engine;playwright" } - // }, - "digikey.com": { - scrapeOptions: {}, - internalOptions: { forceEngine: "fire-engine;tlsclient" } - }, - "lorealparis.hu": { - scrapeOptions: {}, - internalOptions: { forceEngine: "fire-engine;tlsclient" }, - } + // "support.greenpay.me": docsParam, + // "docs.pdw.co": docsParam, + // "developers.notion.com": docsParam, + // "docs2.hubitat.com": docsParam, + // "rsseau.fr": docsParam, + // "help.salesforce.com": docsParam, + // "scrapethissite.com": { + // scrapeOptions: {}, + // internalOptions: { forceEngine: "fetch" }, + // }, + // "eonhealth.com": { + // defaultScraper: "fire-engine", + // params: { + // fireEngineOptions: { + // mobileProxy: true, + // method: "get", + // engine: "request", + // }, + // }, + // }, + // "notion.com": { + // scrapeOptions: { waitFor: 2000 }, + // internalOptions: { forceEngine: "fire-engine;playwright" } + // }, + // "developer.apple.com": { + // scrapeOptions: { waitFor: 2000 }, + // internalOptions: { forceEngine: "fire-engine;playwright" } + // }, + "digikey.com": { + scrapeOptions: {}, + internalOptions: { forceEngine: "fire-engine;tlsclient" } + }, + "lorealparis.hu": { + scrapeOptions: {}, + internalOptions: { forceEngine: "fire-engine;tlsclient" } + } }; diff --git a/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts b/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts index 23cf253b..8bef0c2c 100644 --- a/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts +++ b/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts @@ -7,384 +7,485 @@ import { scrapeOptions } from "../../controllers/v1/types"; import { Engine } from "./engines"; const testEngines: (Engine | undefined)[] = [ - undefined, - "fire-engine;chrome-cdp", - "fire-engine;playwright", - "fire-engine;tlsclient", - "scrapingbee", - "scrapingbeeLoad", - "fetch", + undefined, + "fire-engine;chrome-cdp", + "fire-engine;playwright", + "fire-engine;tlsclient", + "scrapingbee", + "scrapingbeeLoad", + "fetch" ]; const testEnginesScreenshot: (Engine | undefined)[] = [ - undefined, - "fire-engine;chrome-cdp", - "fire-engine;playwright", - "scrapingbee", - "scrapingbeeLoad", + undefined, + "fire-engine;chrome-cdp", + "fire-engine;playwright", + "scrapingbee", + "scrapingbeeLoad" ]; describe("Standalone scrapeURL tests", () => { - describe.each(testEngines)("Engine %s", (forceEngine: Engine | undefined) => { - it("Basic scrape", async () => { - const out = await scrapeURL("test:scrape-basic", "https://www.roastmywebsite.ai/", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).not.toHaveProperty("content"); - expect(out.document).toHaveProperty("markdown"); - expect(out.document).toHaveProperty("metadata"); - expect(out.document).not.toHaveProperty("html"); - expect(out.document.markdown).toContain("_Roast_"); - expect(out.document.metadata.error).toBeUndefined(); - expect(out.document.metadata.title).toBe("Roast My Website"); - expect(out.document.metadata.description).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" - ); - expect(out.document.metadata.keywords).toBe( - "Roast My Website,Roast,Website,GitHub,Firecrawl" - ); - expect(out.document.metadata.robots).toBe("follow, index"); - expect(out.document.metadata.ogTitle).toBe("Roast My Website"); - expect(out.document.metadata.ogDescription).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" - ); - expect(out.document.metadata.ogUrl).toBe( - "https://www.roastmywebsite.ai" - ); - expect(out.document.metadata.ogImage).toBe( - "https://www.roastmywebsite.ai/og.png" - ); - expect(out.document.metadata.ogLocaleAlternate).toStrictEqual([]); - expect(out.document.metadata.ogSiteName).toBe("Roast My Website"); - expect(out.document.metadata.sourceURL).toBe( - "https://www.roastmywebsite.ai/" - ); - expect(out.document.metadata.statusCode).toBe(200); - } - - }, 30000); - - it("Scrape with formats markdown and html", async () => { - const out = await scrapeURL("test:scrape-formats-markdown-html", "https://roastmywebsite.ai", scrapeOptions.parse({ - formats: ["markdown", "html"], - }), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty("markdown"); - expect(out.document).toHaveProperty("html"); - expect(out.document).toHaveProperty("metadata"); - expect(out.document.markdown).toContain("_Roast_"); - expect(out.document.html).toContain(" { - const out = await scrapeURL("test:scrape-onlyMainContent-false", "https://www.scrapethissite.com/", scrapeOptions.parse({ - onlyMainContent: false, - }), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty("markdown"); - expect(out.document).toHaveProperty("metadata"); - expect(out.document).not.toHaveProperty("html"); - expect(out.document.markdown).toContain("[FAQ](/faq/)"); // .nav - expect(out.document.markdown).toContain("Hartley Brody 2023"); // #footer - } - }, 30000); - - it("Scrape with excludeTags", async () => { - const out = await scrapeURL("test:scrape-excludeTags", "https://www.scrapethissite.com/", scrapeOptions.parse({ - onlyMainContent: false, - excludeTags: ['.nav', '#footer', 'strong'], - }), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty("markdown"); - expect(out.document).toHaveProperty("metadata"); - expect(out.document).not.toHaveProperty("html"); - expect(out.document.markdown).not.toContain("Hartley Brody 2023"); - expect(out.document.markdown).not.toContain("[FAQ](/faq/)"); - } - }, 30000); - - it("Scrape of a page with 400 status code", async () => { - const out = await scrapeURL("test:scrape-400", "https://httpstat.us/400", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(400); - } - }, 30000); - - it("Scrape of a page with 401 status code", async () => { - const out = await scrapeURL("test:scrape-401", "https://httpstat.us/401", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(401); - } - }, 30000); - - it("Scrape of a page with 403 status code", async () => { - const out = await scrapeURL("test:scrape-403", "https://httpstat.us/403", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(403); - } - }, 30000); - - it("Scrape of a page with 404 status code", async () => { - const out = await scrapeURL("test:scrape-404", "https://httpstat.us/404", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(404); - } - }, 30000); - - it("Scrape of a page with 405 status code", async () => { - const out = await scrapeURL("test:scrape-405", "https://httpstat.us/405", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(405); - } - }, 30000); - - it("Scrape of a page with 500 status code", async () => { - const out = await scrapeURL("test:scrape-500", "https://httpstat.us/500", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(500); - } - }, 30000); + describe.each(testEngines)("Engine %s", (forceEngine: Engine | undefined) => { + it("Basic scrape", async () => { + const out = await scrapeURL( + "test:scrape-basic", + "https://www.roastmywebsite.ai/", + scrapeOptions.parse({}), + { forceEngine } + ); - it("Scrape a redirected page", async () => { - const out = await scrapeURL("test:scrape-redirect", "https://scrapethissite.com/", scrapeOptions.parse({}), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document.markdown).toContain("Explore Sandbox"); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.sourceURL).toBe("https://scrapethissite.com/"); - expect(out.document.metadata.url).toBe("https://www.scrapethissite.com/"); - expect(out.document.metadata.statusCode).toBe(200); - expect(out.document.metadata.error).toBeUndefined(); - } - }, 30000); - }); - - describe.each(testEnginesScreenshot)("Screenshot on engine %s", (forceEngine: Engine | undefined) => { - it("Scrape with screenshot", async () => { - const out = await scrapeURL("test:scrape-screenshot", "https://www.scrapethissite.com/", scrapeOptions.parse({ - formats: ["screenshot"], - }), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('screenshot'); - expect(typeof out.document.screenshot).toBe("string"); - expect(out.document.screenshot!.startsWith("https://service.firecrawl.dev/storage/v1/object/public/media/")); - // TODO: attempt to fetch screenshot - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(200); - expect(out.document.metadata.error).toBeUndefined(); - } - }, 30000); - - it("Scrape with full-page screenshot", async () => { - const out = await scrapeURL("test:scrape-screenshot-fullPage", "https://www.scrapethissite.com/", scrapeOptions.parse({ - formats: ["screenshot@fullPage"], - }), { forceEngine }); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('screenshot'); - expect(typeof out.document.screenshot).toBe("string"); - expect(out.document.screenshot!.startsWith("https://service.firecrawl.dev/storage/v1/object/public/media/")); - // TODO: attempt to fetch screenshot - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.statusCode).toBe(200); - expect(out.document.metadata.error).toBeUndefined(); - } - }, 30000); - }); - - it("Scrape of a PDF file", async () => { - const out = await scrapeURL("test:scrape-pdf", "https://arxiv.org/pdf/astro-ph/9301001.pdf", scrapeOptions.parse({})); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.markdown).toContain('Broad Line Radio Galaxy'); - expect(out.document.metadata.statusCode).toBe(200); - expect(out.document.metadata.error).toBeUndefined(); - } - }, 60000); - - it("Scrape a DOCX file", async () => { - const out = await scrapeURL("test:scrape-docx", "https://nvca.org/wp-content/uploads/2019/06/NVCA-Model-Document-Stock-Purchase-Agreement.docx", scrapeOptions.parse({})); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.markdown).toContain('SERIES A PREFERRED STOCK PURCHASE AGREEMENT'); - expect(out.document.metadata.statusCode).toBe(200); - expect(out.document.metadata.error).toBeUndefined(); - } - }, 60000) - - it("LLM extract with prompt and schema", async () => { - const out = await scrapeURL("test:llm-extract-prompt-schema", "https://firecrawl.dev", scrapeOptions.parse({ - formats: ["extract"], - extract: { - prompt: "Based on the information on the page, find what the company's mission is and whether it supports SSO, and whether it is open source", - schema: { - type: "object", - properties: { - company_mission: { type: "string" }, - supports_sso: { type: "boolean" }, - is_open_source: { type: "boolean" }, - }, - required: ["company_mission", "supports_sso", "is_open_source"], - additionalProperties: false, - }, - }, - })); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty("extract"); - expect(out.document.extract).toHaveProperty("company_mission"); - expect(out.document.extract).toHaveProperty("supports_sso"); - expect(out.document.extract).toHaveProperty("is_open_source"); - expect(typeof out.document.extract.company_mission).toBe("string"); - expect(out.document.extract.supports_sso).toBe(false); - expect(out.document.extract.is_open_source).toBe(true); - } - }, 120000) - - it("LLM extract with schema only", async () => { - const out = await scrapeURL("test:llm-extract-schema", "https://firecrawl.dev", scrapeOptions.parse({ - formats: ["extract"], - extract: { - schema: { - type: "object", - properties: { - company_mission: { type: "string" }, - supports_sso: { type: "boolean" }, - is_open_source: { type: "boolean" }, - }, - required: ["company_mission", "supports_sso", "is_open_source"], - additionalProperties: false, - }, - }, - })); - - // expect(out.logs.length).toBeGreaterThan(0); - expect(out.success).toBe(true); - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty("extract"); - expect(out.document.extract).toHaveProperty("company_mission"); - expect(out.document.extract).toHaveProperty("supports_sso"); - expect(out.document.extract).toHaveProperty("is_open_source"); - expect(typeof out.document.extract.company_mission).toBe("string"); - expect(out.document.extract.supports_sso).toBe(false); - expect(out.document.extract.is_open_source).toBe(true); - } - }, 120000) - - test.concurrent.each(new Array(100).fill(0).map((_, i) => i))("Concurrent scrape #%i", async (i) => { - const url = "https://www.scrapethissite.com/?i=" + i; - const id = "test:concurrent:" + url; - const out = await scrapeURL(id, url, scrapeOptions.parse({})); - - const replacer = (key: string, value: any) => { - if (value instanceof Error) { - return { - ...value, - message: value.message, - name: value.name, - cause: value.cause, - stack: value.stack, - } - } else { - return value; - } - } - - // verify that log collection works properly while concurrency is happening - // expect(out.logs.length).toBeGreaterThan(0); - const weirdLogs = out.logs.filter(x => x.scrapeId !== id); - if (weirdLogs.length > 0) { - console.warn(JSON.stringify(weirdLogs, replacer)); - } - expect(weirdLogs.length).toBe(0); - - if (!out.success) console.error(JSON.stringify(out, replacer)); - expect(out.success).toBe(true); - - if (out.success) { - expect(out.document.warning).toBeUndefined(); - expect(out.document).toHaveProperty('markdown'); - expect(out.document).toHaveProperty('metadata'); - expect(out.document.metadata.error).toBeUndefined(); - expect(out.document.metadata.statusCode).toBe(200); - } + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).not.toHaveProperty("content"); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document).not.toHaveProperty("html"); + expect(out.document.markdown).toContain("_Roast_"); + expect(out.document.metadata.error).toBeUndefined(); + expect(out.document.metadata.title).toBe("Roast My Website"); + expect(out.document.metadata.description).toBe( + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + ); + expect(out.document.metadata.keywords).toBe( + "Roast My Website,Roast,Website,GitHub,Firecrawl" + ); + expect(out.document.metadata.robots).toBe("follow, index"); + expect(out.document.metadata.ogTitle).toBe("Roast My Website"); + expect(out.document.metadata.ogDescription).toBe( + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + ); + expect(out.document.metadata.ogUrl).toBe( + "https://www.roastmywebsite.ai" + ); + expect(out.document.metadata.ogImage).toBe( + "https://www.roastmywebsite.ai/og.png" + ); + expect(out.document.metadata.ogLocaleAlternate).toStrictEqual([]); + expect(out.document.metadata.ogSiteName).toBe("Roast My Website"); + expect(out.document.metadata.sourceURL).toBe( + "https://www.roastmywebsite.ai/" + ); + expect(out.document.metadata.statusCode).toBe(200); + } }, 30000); -}) + + it("Scrape with formats markdown and html", async () => { + const out = await scrapeURL( + "test:scrape-formats-markdown-html", + "https://roastmywebsite.ai", + scrapeOptions.parse({ + formats: ["markdown", "html"] + }), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("html"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.markdown).toContain("_Roast_"); + expect(out.document.html).toContain(" { + const out = await scrapeURL( + "test:scrape-onlyMainContent-false", + "https://www.scrapethissite.com/", + scrapeOptions.parse({ + onlyMainContent: false + }), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document).not.toHaveProperty("html"); + expect(out.document.markdown).toContain("[FAQ](/faq/)"); // .nav + expect(out.document.markdown).toContain("Hartley Brody 2023"); // #footer + } + }, 30000); + + it("Scrape with excludeTags", async () => { + const out = await scrapeURL( + "test:scrape-excludeTags", + "https://www.scrapethissite.com/", + scrapeOptions.parse({ + onlyMainContent: false, + excludeTags: [".nav", "#footer", "strong"] + }), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document).not.toHaveProperty("html"); + expect(out.document.markdown).not.toContain("Hartley Brody 2023"); + expect(out.document.markdown).not.toContain("[FAQ](/faq/)"); + } + }, 30000); + + it("Scrape of a page with 400 status code", async () => { + const out = await scrapeURL( + "test:scrape-400", + "https://httpstat.us/400", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(400); + } + }, 30000); + + it("Scrape of a page with 401 status code", async () => { + const out = await scrapeURL( + "test:scrape-401", + "https://httpstat.us/401", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(401); + } + }, 30000); + + it("Scrape of a page with 403 status code", async () => { + const out = await scrapeURL( + "test:scrape-403", + "https://httpstat.us/403", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(403); + } + }, 30000); + + it("Scrape of a page with 404 status code", async () => { + const out = await scrapeURL( + "test:scrape-404", + "https://httpstat.us/404", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(404); + } + }, 30000); + + it("Scrape of a page with 405 status code", async () => { + const out = await scrapeURL( + "test:scrape-405", + "https://httpstat.us/405", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(405); + } + }, 30000); + + it("Scrape of a page with 500 status code", async () => { + const out = await scrapeURL( + "test:scrape-500", + "https://httpstat.us/500", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(500); + } + }, 30000); + + it("Scrape a redirected page", async () => { + const out = await scrapeURL( + "test:scrape-redirect", + "https://scrapethissite.com/", + scrapeOptions.parse({}), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document.markdown).toContain("Explore Sandbox"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.sourceURL).toBe( + "https://scrapethissite.com/" + ); + expect(out.document.metadata.url).toBe( + "https://www.scrapethissite.com/" + ); + expect(out.document.metadata.statusCode).toBe(200); + expect(out.document.metadata.error).toBeUndefined(); + } + }, 30000); + }); + + describe.each(testEnginesScreenshot)( + "Screenshot on engine %s", + (forceEngine: Engine | undefined) => { + it("Scrape with screenshot", async () => { + const out = await scrapeURL( + "test:scrape-screenshot", + "https://www.scrapethissite.com/", + scrapeOptions.parse({ + formats: ["screenshot"] + }), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("screenshot"); + expect(typeof out.document.screenshot).toBe("string"); + expect( + out.document.screenshot!.startsWith( + "https://service.firecrawl.dev/storage/v1/object/public/media/" + ) + ); + // TODO: attempt to fetch screenshot + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(200); + expect(out.document.metadata.error).toBeUndefined(); + } + }, 30000); + + it("Scrape with full-page screenshot", async () => { + const out = await scrapeURL( + "test:scrape-screenshot-fullPage", + "https://www.scrapethissite.com/", + scrapeOptions.parse({ + formats: ["screenshot@fullPage"] + }), + { forceEngine } + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("screenshot"); + expect(typeof out.document.screenshot).toBe("string"); + expect( + out.document.screenshot!.startsWith( + "https://service.firecrawl.dev/storage/v1/object/public/media/" + ) + ); + // TODO: attempt to fetch screenshot + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.statusCode).toBe(200); + expect(out.document.metadata.error).toBeUndefined(); + } + }, 30000); + } + ); + + it("Scrape of a PDF file", async () => { + const out = await scrapeURL( + "test:scrape-pdf", + "https://arxiv.org/pdf/astro-ph/9301001.pdf", + scrapeOptions.parse({}) + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.markdown).toContain("Broad Line Radio Galaxy"); + expect(out.document.metadata.statusCode).toBe(200); + expect(out.document.metadata.error).toBeUndefined(); + } + }, 60000); + + it("Scrape a DOCX file", async () => { + const out = await scrapeURL( + "test:scrape-docx", + "https://nvca.org/wp-content/uploads/2019/06/NVCA-Model-Document-Stock-Purchase-Agreement.docx", + scrapeOptions.parse({}) + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.markdown).toContain( + "SERIES A PREFERRED STOCK PURCHASE AGREEMENT" + ); + expect(out.document.metadata.statusCode).toBe(200); + expect(out.document.metadata.error).toBeUndefined(); + } + }, 60000); + + it("LLM extract with prompt and schema", async () => { + const out = await scrapeURL( + "test:llm-extract-prompt-schema", + "https://firecrawl.dev", + scrapeOptions.parse({ + formats: ["extract"], + extract: { + prompt: + "Based on the information on the page, find what the company's mission is and whether it supports SSO, and whether it is open source", + schema: { + type: "object", + properties: { + company_mission: { type: "string" }, + supports_sso: { type: "boolean" }, + is_open_source: { type: "boolean" } + }, + required: ["company_mission", "supports_sso", "is_open_source"], + additionalProperties: false + } + } + }) + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("extract"); + expect(out.document.extract).toHaveProperty("company_mission"); + expect(out.document.extract).toHaveProperty("supports_sso"); + expect(out.document.extract).toHaveProperty("is_open_source"); + expect(typeof out.document.extract.company_mission).toBe("string"); + expect(out.document.extract.supports_sso).toBe(false); + expect(out.document.extract.is_open_source).toBe(true); + } + }, 120000); + + it("LLM extract with schema only", async () => { + const out = await scrapeURL( + "test:llm-extract-schema", + "https://firecrawl.dev", + scrapeOptions.parse({ + formats: ["extract"], + extract: { + schema: { + type: "object", + properties: { + company_mission: { type: "string" }, + supports_sso: { type: "boolean" }, + is_open_source: { type: "boolean" } + }, + required: ["company_mission", "supports_sso", "is_open_source"], + additionalProperties: false + } + } + }) + ); + + // expect(out.logs.length).toBeGreaterThan(0); + expect(out.success).toBe(true); + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("extract"); + expect(out.document.extract).toHaveProperty("company_mission"); + expect(out.document.extract).toHaveProperty("supports_sso"); + expect(out.document.extract).toHaveProperty("is_open_source"); + expect(typeof out.document.extract.company_mission).toBe("string"); + expect(out.document.extract.supports_sso).toBe(false); + expect(out.document.extract.is_open_source).toBe(true); + } + }, 120000); + + test.concurrent.each(new Array(100).fill(0).map((_, i) => i))( + "Concurrent scrape #%i", + async (i) => { + const url = "https://www.scrapethissite.com/?i=" + i; + const id = "test:concurrent:" + url; + const out = await scrapeURL(id, url, scrapeOptions.parse({})); + + const replacer = (key: string, value: any) => { + if (value instanceof Error) { + return { + ...value, + message: value.message, + name: value.name, + cause: value.cause, + stack: value.stack + }; + } else { + return value; + } + }; + + // verify that log collection works properly while concurrency is happening + // expect(out.logs.length).toBeGreaterThan(0); + const weirdLogs = out.logs.filter((x) => x.scrapeId !== id); + if (weirdLogs.length > 0) { + console.warn(JSON.stringify(weirdLogs, replacer)); + } + expect(weirdLogs.length).toBe(0); + + if (!out.success) console.error(JSON.stringify(out, replacer)); + expect(out.success).toBe(true); + + if (out.success) { + expect(out.document.warning).toBeUndefined(); + expect(out.document).toHaveProperty("markdown"); + expect(out.document).toHaveProperty("metadata"); + expect(out.document.metadata.error).toBeUndefined(); + expect(out.document.metadata.statusCode).toBe(200); + } + }, + 30000 + ); +}); diff --git a/apps/api/src/scraper/scrapeURL/transformers/cache.ts b/apps/api/src/scraper/scrapeURL/transformers/cache.ts index e0c09c44..4a31da1f 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/cache.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/cache.ts @@ -3,24 +3,30 @@ import { Meta } from ".."; import { CacheEntry, cacheKey, saveEntryToCache } from "../../../lib/cache"; export function saveToCache(meta: Meta, document: Document): Document { - if (document.metadata.statusCode! < 200 || document.metadata.statusCode! >= 300) return document; - - if (document.rawHtml === undefined) { - throw new Error("rawHtml is undefined -- this transformer is being called out of order"); - } - - const key = cacheKey(meta.url, meta.options, meta.internalOptions); - - if (key !== null) { - const entry: CacheEntry = { - html: document.rawHtml!, - statusCode: document.metadata.statusCode!, - url: document.metadata.url ?? document.metadata.sourceURL!, - error: document.metadata.error ?? undefined, - }; - - saveEntryToCache(key, entry); - } - + if ( + document.metadata.statusCode! < 200 || + document.metadata.statusCode! >= 300 + ) return document; -} \ No newline at end of file + + if (document.rawHtml === undefined) { + throw new Error( + "rawHtml is undefined -- this transformer is being called out of order" + ); + } + + const key = cacheKey(meta.url, meta.options, meta.internalOptions); + + if (key !== null) { + const entry: CacheEntry = { + html: document.rawHtml!, + statusCode: document.metadata.statusCode!, + url: document.metadata.url ?? document.metadata.sourceURL!, + error: document.metadata.error ?? undefined + }; + + saveEntryToCache(key, entry); + } + + return document; +} diff --git a/apps/api/src/scraper/scrapeURL/transformers/index.ts b/apps/api/src/scraper/scrapeURL/transformers/index.ts index b8063f7e..5afceda2 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/index.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/index.ts @@ -9,127 +9,180 @@ import { uploadScreenshot } from "./uploadScreenshot"; import { removeBase64Images } from "./removeBase64Images"; import { saveToCache } from "./cache"; -export type Transformer = (meta: Meta, document: Document) => Document | Promise; +export type Transformer = ( + meta: Meta, + document: Document +) => Document | Promise; -export function deriveMetadataFromRawHTML(meta: Meta, document: Document): Document { - if (document.rawHtml === undefined) { - throw new Error("rawHtml is undefined -- this transformer is being called out of order"); - } +export function deriveMetadataFromRawHTML( + meta: Meta, + document: Document +): Document { + if (document.rawHtml === undefined) { + throw new Error( + "rawHtml is undefined -- this transformer is being called out of order" + ); + } - document.metadata = { - ...extractMetadata(meta, document.rawHtml), - ...document.metadata, - }; - return document; + document.metadata = { + ...extractMetadata(meta, document.rawHtml), + ...document.metadata + }; + return document; } -export function deriveHTMLFromRawHTML(meta: Meta, document: Document): Document { - if (document.rawHtml === undefined) { - throw new Error("rawHtml is undefined -- this transformer is being called out of order"); - } +export function deriveHTMLFromRawHTML( + meta: Meta, + document: Document +): Document { + if (document.rawHtml === undefined) { + throw new Error( + "rawHtml is undefined -- this transformer is being called out of order" + ); + } - document.html = removeUnwantedElements(document.rawHtml, meta.options); - return document; + document.html = removeUnwantedElements(document.rawHtml, meta.options); + return document; } -export async function deriveMarkdownFromHTML(_meta: Meta, document: Document): Promise { - if (document.html === undefined) { - throw new Error("html is undefined -- this transformer is being called out of order"); - } +export async function deriveMarkdownFromHTML( + _meta: Meta, + document: Document +): Promise { + if (document.html === undefined) { + throw new Error( + "html is undefined -- this transformer is being called out of order" + ); + } - document.markdown = await parseMarkdown(document.html); - return document; + document.markdown = await parseMarkdown(document.html); + return document; } export function deriveLinksFromHTML(meta: Meta, document: Document): Document { - // Only derive if the formats has links - if (meta.options.formats.includes("links")) { - if (document.html === undefined) { - throw new Error("html is undefined -- this transformer is being called out of order"); - } - - document.links = extractLinks(document.html, meta.url); + // Only derive if the formats has links + if (meta.options.formats.includes("links")) { + if (document.html === undefined) { + throw new Error( + "html is undefined -- this transformer is being called out of order" + ); } - return document; + document.links = extractLinks(document.html, meta.url); + } + + return document; } -export function coerceFieldsToFormats(meta: Meta, document: Document): Document { - const formats = new Set(meta.options.formats); +export function coerceFieldsToFormats( + meta: Meta, + document: Document +): Document { + const formats = new Set(meta.options.formats); - if (!formats.has("markdown") && document.markdown !== undefined) { - delete document.markdown; - } else if (formats.has("markdown") && document.markdown === undefined) { - meta.logger.warn("Request had format: markdown, but there was no markdown field in the result."); - } + if (!formats.has("markdown") && document.markdown !== undefined) { + delete document.markdown; + } else if (formats.has("markdown") && document.markdown === undefined) { + meta.logger.warn( + "Request had format: markdown, but there was no markdown field in the result." + ); + } - if (!formats.has("rawHtml") && document.rawHtml !== undefined) { - delete document.rawHtml; - } else if (formats.has("rawHtml") && document.rawHtml === undefined) { - meta.logger.warn("Request had format: rawHtml, but there was no rawHtml field in the result."); - } + if (!formats.has("rawHtml") && document.rawHtml !== undefined) { + delete document.rawHtml; + } else if (formats.has("rawHtml") && document.rawHtml === undefined) { + meta.logger.warn( + "Request had format: rawHtml, but there was no rawHtml field in the result." + ); + } - if (!formats.has("html") && document.html !== undefined) { - delete document.html; - } else if (formats.has("html") && document.html === undefined) { - meta.logger.warn("Request had format: html, but there was no html field in the result."); - } + if (!formats.has("html") && document.html !== undefined) { + delete document.html; + } else if (formats.has("html") && document.html === undefined) { + meta.logger.warn( + "Request had format: html, but there was no html field in the result." + ); + } - if (!formats.has("screenshot") && !formats.has("screenshot@fullPage") && document.screenshot !== undefined) { - meta.logger.warn("Removed screenshot from Document because it wasn't in formats -- this is very wasteful and indicates a bug."); - delete document.screenshot; - } else if ((formats.has("screenshot") || formats.has("screenshot@fullPage")) && document.screenshot === undefined) { - meta.logger.warn("Request had format: screenshot / screenshot@fullPage, but there was no screenshot field in the result."); - } + if ( + !formats.has("screenshot") && + !formats.has("screenshot@fullPage") && + document.screenshot !== undefined + ) { + meta.logger.warn( + "Removed screenshot from Document because it wasn't in formats -- this is very wasteful and indicates a bug." + ); + delete document.screenshot; + } else if ( + (formats.has("screenshot") || formats.has("screenshot@fullPage")) && + document.screenshot === undefined + ) { + meta.logger.warn( + "Request had format: screenshot / screenshot@fullPage, but there was no screenshot field in the result." + ); + } - if (!formats.has("links") && document.links !== undefined) { - meta.logger.warn("Removed links from Document because it wasn't in formats -- this is wasteful and indicates a bug."); - delete document.links; - } else if (formats.has("links") && document.links === undefined) { - meta.logger.warn("Request had format: links, but there was no links field in the result."); - } + if (!formats.has("links") && document.links !== undefined) { + meta.logger.warn( + "Removed links from Document because it wasn't in formats -- this is wasteful and indicates a bug." + ); + delete document.links; + } else if (formats.has("links") && document.links === undefined) { + meta.logger.warn( + "Request had format: links, but there was no links field in the result." + ); + } - if (!formats.has("extract") && document.extract !== undefined) { - meta.logger.warn("Removed extract from Document because it wasn't in formats -- this is extremely wasteful and indicates a bug."); - delete document.extract; - } else if (formats.has("extract") && document.extract === undefined) { - meta.logger.warn("Request had format: extract, but there was no extract field in the result."); - } + if (!formats.has("extract") && document.extract !== undefined) { + meta.logger.warn( + "Removed extract from Document because it wasn't in formats -- this is extremely wasteful and indicates a bug." + ); + delete document.extract; + } else if (formats.has("extract") && document.extract === undefined) { + meta.logger.warn( + "Request had format: extract, but there was no extract field in the result." + ); + } - if (meta.options.actions === undefined || meta.options.actions.length === 0) { - delete document.actions; - } + if (meta.options.actions === undefined || meta.options.actions.length === 0) { + delete document.actions; + } - return document; + return document; } // TODO: allow some of these to run in parallel export const transformerStack: Transformer[] = [ - saveToCache, - deriveHTMLFromRawHTML, - deriveMarkdownFromHTML, - deriveLinksFromHTML, - deriveMetadataFromRawHTML, - uploadScreenshot, - performLLMExtract, - coerceFieldsToFormats, - removeBase64Images, + saveToCache, + deriveHTMLFromRawHTML, + deriveMarkdownFromHTML, + deriveLinksFromHTML, + deriveMetadataFromRawHTML, + uploadScreenshot, + performLLMExtract, + coerceFieldsToFormats, + removeBase64Images ]; -export async function executeTransformers(meta: Meta, document: Document): Promise { - const executions: [string, number][] = []; +export async function executeTransformers( + meta: Meta, + document: Document +): Promise { + const executions: [string, number][] = []; - for (const transformer of transformerStack) { - const _meta = { - ...meta, - logger: meta.logger.child({ method: "executeTransformers/" + transformer.name }), - }; - const start = Date.now(); - document = await transformer(_meta, document); - executions.push([transformer.name, Date.now() - start]); - } + for (const transformer of transformerStack) { + const _meta = { + ...meta, + logger: meta.logger.child({ + method: "executeTransformers/" + transformer.name + }) + }; + const start = Date.now(); + document = await transformer(_meta, document); + executions.push([transformer.name, Date.now() - start]); + } - meta.logger.debug("Executed transformers.", { executions }); + meta.logger.debug("Executed transformers.", { executions }); - return document; + return document; } diff --git a/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts b/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts index 1c6adcd1..f09073ee 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts @@ -9,186 +9,226 @@ const maxTokens = 32000; const modifier = 4; export class LLMRefusalError extends Error { - public refusal: string; - public results: EngineResultsTracker | undefined; + public refusal: string; + public results: EngineResultsTracker | undefined; - constructor(refusal: string) { - super("LLM refused to extract the website's content") - this.refusal = refusal; - } + constructor(refusal: string) { + super("LLM refused to extract the website's content"); + this.refusal = refusal; + } } function normalizeSchema(x: any): any { - if (typeof x !== "object" || x === null) return x; + if (typeof x !== "object" || x === null) return x; - if (x["$defs"] !== null && typeof x["$defs"] === "object") { - x["$defs"] = Object.fromEntries(Object.entries(x["$defs"]).map(([name, schema]) => [name, normalizeSchema(schema)])); - } + if (x["$defs"] !== null && typeof x["$defs"] === "object") { + x["$defs"] = Object.fromEntries( + Object.entries(x["$defs"]).map(([name, schema]) => [ + name, + normalizeSchema(schema) + ]) + ); + } - if (x && x.anyOf) { - x.anyOf = x.anyOf.map(x => normalizeSchema(x)); - } + if (x && x.anyOf) { + x.anyOf = x.anyOf.map((x) => normalizeSchema(x)); + } - if (x && x.oneOf) { - x.oneOf = x.oneOf.map(x => normalizeSchema(x)); - } + if (x && x.oneOf) { + x.oneOf = x.oneOf.map((x) => normalizeSchema(x)); + } - if (x && x.allOf) { - x.allOf = x.allOf.map(x => normalizeSchema(x)); - } + if (x && x.allOf) { + x.allOf = x.allOf.map((x) => normalizeSchema(x)); + } - if (x && x.not) { - x.not = normalizeSchema(x.not); - } + if (x && x.not) { + x.not = normalizeSchema(x.not); + } - if (x && x.type === "object") { - return { - ...x, - properties: Object.fromEntries(Object.entries(x.properties).map(([k, v]) => [k, normalizeSchema(v)])), - required: Object.keys(x.properties), - additionalProperties: false, - } - } else if (x && x.type === "array") { - return { - ...x, - items: normalizeSchema(x.items), - } - } else { - return x; - } + if (x && x.type === "object") { + return { + ...x, + properties: Object.fromEntries( + Object.entries(x.properties).map(([k, v]) => [k, normalizeSchema(v)]) + ), + required: Object.keys(x.properties), + additionalProperties: false + }; + } else if (x && x.type === "array") { + return { + ...x, + items: normalizeSchema(x.items) + }; + } else { + return x; + } } -export async function generateOpenAICompletions(logger: Logger, options: ExtractOptions, markdown?: string, previousWarning?: string, isExtractEndpoint?: boolean): Promise<{ extract: any, numTokens: number, warning: string | undefined }> { - let extract: any; - let warning: string | undefined; +export async function generateOpenAICompletions( + logger: Logger, + options: ExtractOptions, + markdown?: string, + previousWarning?: string, + isExtractEndpoint?: boolean +): Promise<{ extract: any; numTokens: number; warning: string | undefined }> { + let extract: any; + let warning: string | undefined; - const openai = new OpenAI(); - const model: TiktokenModel = (process.env.MODEL_NAME as TiktokenModel) ?? "gpt-4o-mini"; + const openai = new OpenAI(); + const model: TiktokenModel = + (process.env.MODEL_NAME as TiktokenModel) ?? "gpt-4o-mini"; - if (markdown === undefined) { - throw new Error("document.markdown is undefined -- this is unexpected"); - } + if (markdown === undefined) { + throw new Error("document.markdown is undefined -- this is unexpected"); + } - // count number of tokens - let numTokens = 0; - const encoder = encoding_for_model(model as TiktokenModel); + // count number of tokens + let numTokens = 0; + const encoder = encoding_for_model(model as TiktokenModel); + try { + // Encode the message into tokens + const tokens = encoder.encode(markdown); + + // Return the number of tokens + numTokens = tokens.length; + } catch (error) { + logger.warn("Calculating num tokens of string failed", { error, markdown }); + + markdown = markdown.slice(0, maxTokens * modifier); + + let w = + "Failed to derive number of LLM tokens the extraction might use -- the input has been automatically trimmed to the maximum number of tokens (" + + maxTokens + + ") we support."; + warning = previousWarning === undefined ? w : w + " " + previousWarning; + } finally { + // Free the encoder resources after use + encoder.free(); + } + + if (numTokens > maxTokens) { + // trim the document to the maximum number of tokens, tokens != characters + markdown = markdown.slice(0, maxTokens * modifier); + + const w = + "The extraction content would have used more tokens (" + + numTokens + + ") than the maximum we allow (" + + maxTokens + + "). -- the input has been automatically trimmed."; + warning = previousWarning === undefined ? w : w + " " + previousWarning; + } + + let schema = options.schema; + if (schema && schema.type === "array") { + schema = { + type: "object", + properties: { + items: options.schema + }, + required: ["items"], + additionalProperties: false + }; + } else if (schema && typeof schema === "object" && !schema.type) { + schema = { + type: "object", + properties: Object.fromEntries( + Object.entries(schema).map(([key, value]) => [key, { type: value }]) + ), + required: Object.keys(schema), + additionalProperties: false + }; + } + + schema = normalizeSchema(schema); + + const jsonCompletion = await openai.beta.chat.completions.parse({ + model, + temperature: 0, + messages: [ + { + role: "system", + content: options.systemPrompt + }, + { + role: "user", + content: [{ type: "text", text: markdown }] + }, + { + role: "user", + content: + options.prompt !== undefined + ? `Transform the above content into structured JSON output based on the following user request: ${options.prompt}` + : "Transform the above content into structured JSON output." + } + ], + response_format: options.schema + ? { + type: "json_schema", + json_schema: { + name: "websiteContent", + schema: schema, + strict: true + } + } + : { type: "json_object" } + }); + + if (jsonCompletion.choices[0].message.refusal !== null) { + throw new LLMRefusalError(jsonCompletion.choices[0].message.refusal); + } + + extract = jsonCompletion.choices[0].message.parsed; + + if (extract === null && jsonCompletion.choices[0].message.content !== null) { try { - // Encode the message into tokens - const tokens = encoder.encode(markdown); - - // Return the number of tokens - numTokens = tokens.length; - } catch (error) { - logger.warn("Calculating num tokens of string failed", { error, markdown }); - - markdown = markdown.slice(0, maxTokens * modifier); - - let w = "Failed to derive number of LLM tokens the extraction might use -- the input has been automatically trimmed to the maximum number of tokens (" + maxTokens + ") we support."; - warning = previousWarning === undefined ? w : w + " " + previousWarning; - } finally { - // Free the encoder resources after use - encoder.free(); - } - - if (numTokens > maxTokens) { - // trim the document to the maximum number of tokens, tokens != characters - markdown = markdown.slice(0, maxTokens * modifier); - - const w = "The extraction content would have used more tokens (" + numTokens + ") than the maximum we allow (" + maxTokens + "). -- the input has been automatically trimmed."; - warning = previousWarning === undefined ? w : w + " " + previousWarning; - } - - let schema = options.schema; - if (schema && schema.type === "array") { - schema = { - type: "object", - properties: { - items: options.schema, - }, - required: ["items"], - additionalProperties: false, - }; - } else if (schema && typeof schema === 'object' && !schema.type) { - schema = { - type: "object", - properties: Object.fromEntries( - Object.entries(schema).map(([key, value]) => [key, { type: value }]) - ), - required: Object.keys(schema), - additionalProperties: false - }; - } - - schema = normalizeSchema(schema); - - const jsonCompletion = await openai.beta.chat.completions.parse({ - model, - temperature: 0, - messages: [ - { - role: "system", - content: options.systemPrompt, - }, - { - role: "user", - content: [{ type: "text", text: markdown }], - }, - { - role: "user", - content: options.prompt !== undefined - ? `Transform the above content into structured JSON output based on the following user request: ${options.prompt}` - : "Transform the above content into structured JSON output.", - }, - ], - response_format: options.schema ? { - type: "json_schema", - json_schema: { - name: "websiteContent", - schema: schema, - strict: true, - } - } : { type: "json_object" }, - }); - - if (jsonCompletion.choices[0].message.refusal !== null) { - throw new LLMRefusalError(jsonCompletion.choices[0].message.refusal); - } - - extract = jsonCompletion.choices[0].message.parsed; - - if (extract === null && jsonCompletion.choices[0].message.content !== null) { - try { - if (!isExtractEndpoint) { - extract = JSON.parse(jsonCompletion.choices[0].message.content); - } else { - const extractData = JSON.parse(jsonCompletion.choices[0].message.content); - extract = options.schema ? extractData.data.extract : extractData; - } - } catch (e) { - logger.error("Failed to parse returned JSON, no schema specified.", { error: e }); - throw new LLMRefusalError("Failed to parse returned JSON. Please specify a schema in the extract object."); - } - } - - // If the users actually wants the items object, they can specify it as 'required' in the schema - // otherwise, we just return the items array - if (options.schema && options.schema.type === "array" && !schema?.required?.includes("items")) { - extract = extract?.items; - } - return { extract, warning, numTokens }; -} - -export async function performLLMExtract(meta: Meta, document: Document): Promise { - if (meta.options.formats.includes("extract")) { - const { extract, warning } = await generateOpenAICompletions( - meta.logger.child({ method: "performLLMExtract/generateOpenAICompletions" }), - meta.options.extract!, - document.markdown, - document.warning, + if (!isExtractEndpoint) { + extract = JSON.parse(jsonCompletion.choices[0].message.content); + } else { + const extractData = JSON.parse( + jsonCompletion.choices[0].message.content ); - document.extract = extract; - document.warning = warning; + extract = options.schema ? extractData.data.extract : extractData; + } + } catch (e) { + logger.error("Failed to parse returned JSON, no schema specified.", { + error: e + }); + throw new LLMRefusalError( + "Failed to parse returned JSON. Please specify a schema in the extract object." + ); } + } - return document; + // If the users actually wants the items object, they can specify it as 'required' in the schema + // otherwise, we just return the items array + if ( + options.schema && + options.schema.type === "array" && + !schema?.required?.includes("items") + ) { + extract = extract?.items; + } + return { extract, warning, numTokens }; +} + +export async function performLLMExtract( + meta: Meta, + document: Document +): Promise { + if (meta.options.formats.includes("extract")) { + const { extract, warning } = await generateOpenAICompletions( + meta.logger.child({ + method: "performLLMExtract/generateOpenAICompletions" + }), + meta.options.extract!, + document.markdown, + document.warning + ); + document.extract = extract; + document.warning = warning; + } + + return document; } diff --git a/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts b/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts index 92628f8a..3bc408ff 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts @@ -4,8 +4,11 @@ import { Document } from "../../../controllers/v1/types"; const regex = /(!\[.*?\])\(data:image\/.*?;base64,.*?\)/g; export function removeBase64Images(meta: Meta, document: Document): Document { - if (meta.options.removeBase64Images && document.markdown !== undefined) { - document.markdown = document.markdown.replace(regex, '$1()'); - } - return document; -} \ No newline at end of file + if (meta.options.removeBase64Images && document.markdown !== undefined) { + document.markdown = document.markdown.replace( + regex, + "$1()" + ); + } + return document; +} diff --git a/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts b/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts index 4c3fc2b4..ed01af69 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts @@ -6,21 +6,29 @@ import { Meta } from ".."; import { Document } from "../../../controllers/v1/types"; export function uploadScreenshot(meta: Meta, document: Document): Document { - if (process.env.USE_DB_AUTHENTICATION === "true" && document.screenshot !== undefined && document.screenshot.startsWith("data:")) { - meta.logger.debug("Uploading screenshot to Supabase..."); + if ( + process.env.USE_DB_AUTHENTICATION === "true" && + document.screenshot !== undefined && + document.screenshot.startsWith("data:") + ) { + meta.logger.debug("Uploading screenshot to Supabase..."); - const fileName = `screenshot-${crypto.randomUUID()}.png`; + const fileName = `screenshot-${crypto.randomUUID()}.png`; - supabase_service.storage - .from("media") - .upload(fileName, Buffer.from(document.screenshot.split(",")[1], "base64"), { - cacheControl: "3600", - upsert: false, - contentType: document.screenshot.split(":")[1].split(";")[0], - }); - - document.screenshot = `https://service.firecrawl.dev/storage/v1/object/public/media/${encodeURIComponent(fileName)}`; - } + supabase_service.storage + .from("media") + .upload( + fileName, + Buffer.from(document.screenshot.split(",")[1], "base64"), + { + cacheControl: "3600", + upsert: false, + contentType: document.screenshot.split(":")[1].split(";")[0] + } + ); - return document; + document.screenshot = `https://service.firecrawl.dev/storage/v1/object/public/media/${encodeURIComponent(fileName)}`; + } + + return document; } diff --git a/apps/api/src/search/fireEngine.ts b/apps/api/src/search/fireEngine.ts index c1417af1..3fa9c588 100644 --- a/apps/api/src/search/fireEngine.ts +++ b/apps/api/src/search/fireEngine.ts @@ -25,7 +25,7 @@ export async function fireEngineMap( location: options.location, tbs: options.tbs, numResults: options.numResults, - page: options.page ?? 1, + page: options.page ?? 1 }); if (!process.env.FIRE_ENGINE_BETA_URL) { @@ -39,9 +39,9 @@ export async function fireEngineMap( method: "POST", headers: { "Content-Type": "application/json", - "X-Disable-Cache": "true", + "X-Disable-Cache": "true" }, - body: data, + body: data }); if (response.ok) { diff --git a/apps/api/src/search/googlesearch.ts b/apps/api/src/search/googlesearch.ts index 59662829..a7c78fc9 100644 --- a/apps/api/src/search/googlesearch.ts +++ b/apps/api/src/search/googlesearch.ts @@ -1,114 +1,150 @@ -import axios from 'axios'; -import * as cheerio from 'cheerio'; -import * as querystring from 'querystring'; -import { SearchResult } from '../../src/lib/entities'; -import { logger } from '../../src/lib/logger'; +import axios from "axios"; +import * as cheerio from "cheerio"; +import * as querystring from "querystring"; +import { SearchResult } from "../../src/lib/entities"; +import { logger } from "../../src/lib/logger"; const _useragent_list = [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.62', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0' + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.62", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0" ]; function get_useragent(): string { - return _useragent_list[Math.floor(Math.random() * _useragent_list.length)]; + return _useragent_list[Math.floor(Math.random() * _useragent_list.length)]; } -async function _req(term: string, results: number, lang: string, country: string, start: number, proxies: any, timeout: number, tbs: string | undefined = undefined, filter: string | undefined = undefined) { - const params = { - "q": term, - "num": results, // Number of results to return - "hl": lang, - "gl": country, - "start": start, - }; - if (tbs) { - params["tbs"] = tbs; +async function _req( + term: string, + results: number, + lang: string, + country: string, + start: number, + proxies: any, + timeout: number, + tbs: string | undefined = undefined, + filter: string | undefined = undefined +) { + const params = { + q: term, + num: results, // Number of results to return + hl: lang, + gl: country, + start: start + }; + if (tbs) { + params["tbs"] = tbs; + } + if (filter) { + params["filter"] = filter; + } + try { + const resp = await axios.get("https://www.google.com/search", { + headers: { + "User-Agent": get_useragent() + }, + params: params, + proxy: proxies, + timeout: timeout + }); + return resp; + } catch (error) { + if (error.response && error.response.status === 429) { + throw new Error("Google Search: Too many requests, try again later."); } - if (filter) { - params["filter"] = filter; + throw error; + } +} + +export async function googleSearch( + term: string, + advanced = false, + num_results = 7, + tbs = undefined as string | undefined, + filter = undefined as string | undefined, + lang = "en", + country = "us", + proxy = undefined as string | undefined, + sleep_interval = 0, + timeout = 5000 +): Promise { + let proxies: any = null; + if (proxy) { + if (proxy.startsWith("https")) { + proxies = { https: proxy }; + } else { + proxies = { http: proxy }; } + } + + // TODO: knowledge graph, answer box, etc. + + let start = 0; + let results: SearchResult[] = []; + let attempts = 0; + const maxAttempts = 20; // Define a maximum number of attempts to prevent infinite loop + while (start < num_results && attempts < maxAttempts) { try { - const resp = await axios.get("https://www.google.com/search", { - headers: { - "User-Agent": get_useragent() - }, - params: params, - proxy: proxies, - timeout: timeout, - }); - return resp; + const resp = await _req( + term, + num_results - start, + lang, + country, + start, + proxies, + timeout, + tbs, + filter + ); + const $ = cheerio.load(resp.data); + const result_block = $("div.g"); + if (result_block.length === 0) { + start += 1; + attempts += 1; + } else { + attempts = 0; // Reset attempts if we have results + } + result_block.each((index, element) => { + const linkElement = $(element).find("a"); + const link = + linkElement && linkElement.attr("href") + ? linkElement.attr("href") + : null; + const title = $(element).find("h3"); + const ogImage = $(element).find("img").eq(1).attr("src"); + const description_box = $(element).find( + "div[style='-webkit-line-clamp:2']" + ); + const answerBox = $(element).find(".mod").text(); + if (description_box) { + const description = description_box.text(); + if (link && title && description) { + start += 1; + results.push(new SearchResult(link, title.text(), description)); + } + } + }); + await new Promise((resolve) => + setTimeout(resolve, sleep_interval * 1000) + ); } catch (error) { - if (error.response && error.response.status === 429) { - throw new Error('Google Search: Too many requests, try again later.'); - } - throw error; + if (error.message === "Too many requests") { + logger.warn("Too many requests, breaking the loop"); + break; + } + throw error; } -} - - - -export async function googleSearch(term: string, advanced = false, num_results = 7, tbs = undefined as string | undefined, filter = undefined as string | undefined, lang = "en", country = "us", proxy = undefined as string | undefined, sleep_interval = 0, timeout = 5000, ) :Promise { - let proxies: any = null; - if (proxy) { - if (proxy.startsWith("https")) { - proxies = {"https": proxy}; - } else { - proxies = {"http": proxy}; - } - } - - // TODO: knowledge graph, answer box, etc. - - let start = 0; - let results : SearchResult[] = []; - let attempts = 0; - const maxAttempts = 20; // Define a maximum number of attempts to prevent infinite loop - while (start < num_results && attempts < maxAttempts) { - try { - const resp = await _req(term, num_results - start, lang, country, start, proxies, timeout, tbs, filter); - const $ = cheerio.load(resp.data); - const result_block = $("div.g"); - if (result_block.length === 0) { - start += 1; - attempts += 1; - } else { - attempts = 0; // Reset attempts if we have results - } - result_block.each((index, element) => { - const linkElement = $(element).find("a"); - const link = linkElement && linkElement.attr("href") ? linkElement.attr("href") : null; - const title = $(element).find("h3"); - const ogImage = $(element).find("img").eq(1).attr("src"); - const description_box = $(element).find("div[style='-webkit-line-clamp:2']"); - const answerBox = $(element).find(".mod").text(); - if (description_box) { - const description = description_box.text(); - if (link && title && description) { - start += 1; - results.push(new SearchResult(link, title.text(), description)); - } - } - }); - await new Promise(resolve => setTimeout(resolve, sleep_interval * 1000)); - } catch (error) { - if (error.message === 'Too many requests') { - logger.warn('Too many requests, breaking the loop'); - break; - } - throw error; - } - - if (start === 0) { - return results; - } - } - if (attempts >= maxAttempts) { - logger.warn('Max attempts reached, breaking the loop'); - } - return results + + if (start === 0) { + return results; + } + } + if (attempts >= maxAttempts) { + logger.warn("Max attempts reached, breaking the loop"); + } + return results; } diff --git a/apps/api/src/search/index.ts b/apps/api/src/search/index.ts index 5899af87..978a57e0 100644 --- a/apps/api/src/search/index.ts +++ b/apps/api/src/search/index.ts @@ -16,7 +16,7 @@ export async function search({ location = undefined, proxy = undefined, sleep_interval = 0, - timeout = 5000, + timeout = 5000 }: { query: string; advanced?: boolean; @@ -38,7 +38,7 @@ export async function search({ filter, lang, country, - location, + location }); } if (process.env.SEARCHAPI_API_KEY) { diff --git a/apps/api/src/search/searchapi.ts b/apps/api/src/search/searchapi.ts index 24778a77..ea21c8d3 100644 --- a/apps/api/src/search/searchapi.ts +++ b/apps/api/src/search/searchapi.ts @@ -14,7 +14,10 @@ interface SearchOptions { page?: number; } -export async function searchapi_search(q: string, options: SearchOptions): Promise { +export async function searchapi_search( + q: string, + options: SearchOptions +): Promise { const params = { q: q, hl: options.lang, @@ -22,7 +25,7 @@ export async function searchapi_search(q: string, options: SearchOptions): Promi location: options.location, num: options.num_results, page: options.page ?? 1, - engine: process.env.SEARCHAPI_ENGINE || "google", + engine: process.env.SEARCHAPI_ENGINE || "google" }; const url = `https://www.searchapi.io/api/v1/search`; @@ -30,14 +33,13 @@ export async function searchapi_search(q: string, options: SearchOptions): Promi try { const response = await axios.get(url, { headers: { - "Authorization": `Bearer ${process.env.SEARCHAPI_API_KEY}`, + Authorization: `Bearer ${process.env.SEARCHAPI_API_KEY}`, "Content-Type": "application/json", - "X-SearchApi-Source": "Firecrawl", + "X-SearchApi-Source": "Firecrawl" }, - params: params, + params: params }); - if (response.status === 401) { throw new Error("Unauthorized. Please check your API key."); } @@ -48,7 +50,7 @@ export async function searchapi_search(q: string, options: SearchOptions): Promi return data.organic_results.map((a: any) => ({ url: a.link, title: a.title, - description: a.snippet, + description: a.snippet })); } else { return []; diff --git a/apps/api/src/search/serper.ts b/apps/api/src/search/serper.ts index be716367..4abf720d 100644 --- a/apps/api/src/search/serper.ts +++ b/apps/api/src/search/serper.ts @@ -4,7 +4,9 @@ import { SearchResult } from "../../src/lib/entities"; dotenv.config(); -export async function serper_search(q, options: { +export async function serper_search( + q, + options: { tbs?: string; filter?: string; lang?: string; @@ -12,7 +14,8 @@ export async function serper_search(q, options: { location?: string; num_results: number; page?: number; -}): Promise { + } +): Promise { let data = JSON.stringify({ q: q, hl: options.lang, @@ -20,7 +23,7 @@ export async function serper_search(q, options: { location: options.location, tbs: options.tbs, num: options.num_results, - page: options.page ?? 1, + page: options.page ?? 1 }); let config = { @@ -28,18 +31,18 @@ export async function serper_search(q, options: { url: "https://google.serper.dev/search", headers: { "X-API-KEY": process.env.SERPER_API_KEY, - "Content-Type": "application/json", + "Content-Type": "application/json" }, - data: data, + data: data }; const response = await axios(config); if (response && response.data && Array.isArray(response.data.organic)) { return response.data.organic.map((a) => ({ url: a.link, title: a.title, - description: a.snippet, + description: a.snippet })); - }else{ + } else { return []; } } diff --git a/apps/api/src/services/alerts/index.ts b/apps/api/src/services/alerts/index.ts index 826bb18e..3aaea3aa 100644 --- a/apps/api/src/services/alerts/index.ts +++ b/apps/api/src/services/alerts/index.ts @@ -54,7 +54,7 @@ export async function checkAlerts() { }; await checkAll(); - // setInterval(checkAll, 10000); // Run every + // setInterval(checkAll, 10000); // Run every } } catch (error) { logger.error(`Failed to initialize alerts: ${error}`); diff --git a/apps/api/src/services/alerts/slack.ts b/apps/api/src/services/alerts/slack.ts index 1eac5343..11280f28 100644 --- a/apps/api/src/services/alerts/slack.ts +++ b/apps/api/src/services/alerts/slack.ts @@ -8,14 +8,14 @@ export async function sendSlackWebhook( ) { const messagePrefix = alertEveryone ? " " : ""; const payload = { - text: `${messagePrefix} ${message}`, + text: `${messagePrefix} ${message}` }; try { const response = await axios.post(webhookUrl, payload, { headers: { - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } }); logger.info("Webhook sent successfully:", response.data); } catch (error) { diff --git a/apps/api/src/services/billing/auto_charge.ts b/apps/api/src/services/billing/auto_charge.ts index 1659a110..3411c921 100644 --- a/apps/api/src/services/billing/auto_charge.ts +++ b/apps/api/src/services/billing/auto_charge.ts @@ -23,7 +23,12 @@ const AUTO_RECHARGE_COOLDOWN = 300; // 5 minutes in seconds export async function autoCharge( chunk: AuthCreditUsageChunk, autoRechargeThreshold: number -): Promise<{ success: boolean; message: string; remainingCredits: number; chunk: AuthCreditUsageChunk }> { +): Promise<{ + success: boolean; + message: string; + remainingCredits: number; + chunk: AuthCreditUsageChunk; +}> { const resource = `auto-recharge:${chunk.team_id}`; const cooldownKey = `auto-recharge-cooldown:${chunk.team_id}`; @@ -32,145 +37,162 @@ export async function autoCharge( // Another check to prevent race conditions, double charging - cool down of 5 minutes const cooldownValue = await getValue(cooldownKey); if (cooldownValue) { - logger.info(`Auto-recharge for team ${chunk.team_id} is in cooldown period`); + logger.info( + `Auto-recharge for team ${chunk.team_id} is in cooldown period` + ); return { success: false, message: "Auto-recharge is in cooldown period", remainingCredits: chunk.remaining_credits, - chunk, + chunk }; } // Use a distributed lock to prevent concurrent auto-charge attempts - return await redlock.using([resource], 5000, async (signal) : Promise<{ success: boolean; message: string; remainingCredits: number; chunk: AuthCreditUsageChunk }> => { - // Recheck the condition inside the lock to prevent race conditions - const updatedChunk = await getACUC(chunk.api_key, false, false); - if ( - updatedChunk && - updatedChunk.remaining_credits < autoRechargeThreshold - ) { - if (chunk.sub_user_id) { - // Fetch the customer's Stripe information - const { data: customer, error: customersError } = - await supabase_service - .from("customers") - .select("id, stripe_customer_id") - .eq("id", chunk.sub_user_id) - .single(); - - if (customersError) { - logger.error(`Error fetching customer data: ${customersError}`); - return { - success: false, - message: "Error fetching customer data", - remainingCredits: chunk.remaining_credits, - chunk, - }; - } + return await redlock.using( + [resource], + 5000, + async ( + signal + ): Promise<{ + success: boolean; + message: string; + remainingCredits: number; + chunk: AuthCreditUsageChunk; + }> => { + // Recheck the condition inside the lock to prevent race conditions + const updatedChunk = await getACUC(chunk.api_key, false, false); + if ( + updatedChunk && + updatedChunk.remaining_credits < autoRechargeThreshold + ) { + if (chunk.sub_user_id) { + // Fetch the customer's Stripe information + const { data: customer, error: customersError } = + await supabase_service + .from("customers") + .select("id, stripe_customer_id") + .eq("id", chunk.sub_user_id) + .single(); - if (customer && customer.stripe_customer_id) { - let issueCreditsSuccess = false; - // Attempt to create a payment intent - const paymentStatus = await createPaymentIntent( - chunk.team_id, - customer.stripe_customer_id - ); - - // If payment is successful or requires further action, issue credits - if ( - paymentStatus.return_status === "succeeded" || - paymentStatus.return_status === "requires_action" - ) { - issueCreditsSuccess = await issueCredits( + if (customersError) { + logger.error(`Error fetching customer data: ${customersError}`); + return { + success: false, + message: "Error fetching customer data", + remainingCredits: chunk.remaining_credits, + chunk + }; + } + + if (customer && customer.stripe_customer_id) { + let issueCreditsSuccess = false; + // Attempt to create a payment intent + const paymentStatus = await createPaymentIntent( chunk.team_id, - AUTO_RECHARGE_CREDITS - ); - } - - // Record the auto-recharge transaction - await supabase_service.from("auto_recharge_transactions").insert({ - team_id: chunk.team_id, - initial_payment_status: paymentStatus.return_status, - credits_issued: issueCreditsSuccess ? AUTO_RECHARGE_CREDITS : 0, - stripe_charge_id: paymentStatus.charge_id, - }); - - // Send a notification if credits were successfully issued - if (issueCreditsSuccess) { - await sendNotification( - chunk.team_id, - NotificationType.AUTO_RECHARGE_SUCCESS, - chunk.sub_current_period_start, - chunk.sub_current_period_end, - chunk, - true + customer.stripe_customer_id ); - // Set cooldown period - await setValue(cooldownKey, 'true', AUTO_RECHARGE_COOLDOWN); - } - - // Reset ACUC cache to reflect the new credit balance - const cacheKeyACUC = `acuc_${chunk.api_key}`; - await deleteKey(cacheKeyACUC); - - if (process.env.SLACK_ADMIN_WEBHOOK_URL) { - const webhookCooldownKey = `webhook_cooldown_${chunk.team_id}`; - const isInCooldown = await getValue(webhookCooldownKey); - - if (!isInCooldown) { - sendSlackWebhook( - `Auto-recharge: Team ${chunk.team_id}. ${AUTO_RECHARGE_CREDITS} credits added. Payment status: ${paymentStatus.return_status}.`, - false, - process.env.SLACK_ADMIN_WEBHOOK_URL - ).catch((error) => { - logger.debug(`Error sending slack notification: ${error}`); - }); - - // Set cooldown for 1 hour - await setValue(webhookCooldownKey, 'true', 60 * 60); + // If payment is successful or requires further action, issue credits + if ( + paymentStatus.return_status === "succeeded" || + paymentStatus.return_status === "requires_action" + ) { + issueCreditsSuccess = await issueCredits( + chunk.team_id, + AUTO_RECHARGE_CREDITS + ); } + + // Record the auto-recharge transaction + await supabase_service.from("auto_recharge_transactions").insert({ + team_id: chunk.team_id, + initial_payment_status: paymentStatus.return_status, + credits_issued: issueCreditsSuccess ? AUTO_RECHARGE_CREDITS : 0, + stripe_charge_id: paymentStatus.charge_id + }); + + // Send a notification if credits were successfully issued + if (issueCreditsSuccess) { + await sendNotification( + chunk.team_id, + NotificationType.AUTO_RECHARGE_SUCCESS, + chunk.sub_current_period_start, + chunk.sub_current_period_end, + chunk, + true + ); + + // Set cooldown period + await setValue(cooldownKey, "true", AUTO_RECHARGE_COOLDOWN); + } + + // Reset ACUC cache to reflect the new credit balance + const cacheKeyACUC = `acuc_${chunk.api_key}`; + await deleteKey(cacheKeyACUC); + + if (process.env.SLACK_ADMIN_WEBHOOK_URL) { + const webhookCooldownKey = `webhook_cooldown_${chunk.team_id}`; + const isInCooldown = await getValue(webhookCooldownKey); + + if (!isInCooldown) { + sendSlackWebhook( + `Auto-recharge: Team ${chunk.team_id}. ${AUTO_RECHARGE_CREDITS} credits added. Payment status: ${paymentStatus.return_status}.`, + false, + process.env.SLACK_ADMIN_WEBHOOK_URL + ).catch((error) => { + logger.debug(`Error sending slack notification: ${error}`); + }); + + // Set cooldown for 1 hour + await setValue(webhookCooldownKey, "true", 60 * 60); + } + } + return { + success: true, + message: "Auto-recharge successful", + remainingCredits: + chunk.remaining_credits + AUTO_RECHARGE_CREDITS, + chunk: { + ...chunk, + remaining_credits: + chunk.remaining_credits + AUTO_RECHARGE_CREDITS + } + }; + } else { + logger.error("No Stripe customer ID found for user"); + return { + success: false, + message: "No Stripe customer ID found for user", + remainingCredits: chunk.remaining_credits, + chunk + }; } - return { - success: true, - message: "Auto-recharge successful", - remainingCredits: chunk.remaining_credits + AUTO_RECHARGE_CREDITS, - chunk: {...chunk, remaining_credits: chunk.remaining_credits + AUTO_RECHARGE_CREDITS}, - }; } else { - logger.error("No Stripe customer ID found for user"); + logger.error("No sub_user_id found in chunk"); return { success: false, - message: "No Stripe customer ID found for user", + message: "No sub_user_id found in chunk", remainingCredits: chunk.remaining_credits, - chunk, + chunk }; } - } else { - logger.error("No sub_user_id found in chunk"); - return { - success: false, - message: "No sub_user_id found in chunk", - remainingCredits: chunk.remaining_credits, - chunk, - }; } + return { + success: false, + message: "No need to auto-recharge", + remainingCredits: chunk.remaining_credits, + chunk + }; } - return { - success: false, - message: "No need to auto-recharge", - remainingCredits: chunk.remaining_credits, - chunk, - }; - - }); + ); } catch (error) { logger.error(`Failed to acquire lock for auto-recharge: ${error}`); return { success: false, message: "Failed to acquire lock for auto-recharge", remainingCredits: chunk.remaining_credits, - chunk, + chunk }; } } diff --git a/apps/api/src/services/billing/credit_billing.ts b/apps/api/src/services/billing/credit_billing.ts index 8558d8ba..f25e165e 100644 --- a/apps/api/src/services/billing/credit_billing.ts +++ b/apps/api/src/services/billing/credit_billing.ts @@ -16,10 +16,22 @@ const FREE_CREDITS = 500; /** * If you do not know the subscription_id in the current context, pass subscription_id as undefined. */ -export async function billTeam(team_id: string, subscription_id: string | null | undefined, credits: number) { - return withAuth(supaBillTeam, { success: true, message: "No DB, bypassed." })(team_id, subscription_id, credits); +export async function billTeam( + team_id: string, + subscription_id: string | null | undefined, + credits: number +) { + return withAuth(supaBillTeam, { success: true, message: "No DB, bypassed." })( + team_id, + subscription_id, + credits + ); } -export async function supaBillTeam(team_id: string, subscription_id: string | null | undefined, credits: number) { +export async function supaBillTeam( + team_id: string, + subscription_id: string | null | undefined, + credits: number +) { if (team_id === "preview") { return { success: true, message: "Preview team, no credits used" }; } @@ -29,7 +41,7 @@ export async function supaBillTeam(team_id: string, subscription_id: string | nu _team_id: team_id, sub_id: subscription_id ?? null, fetch_subscription: subscription_id === undefined, - credits, + credits }); if (error) { @@ -46,7 +58,7 @@ export async function supaBillTeam(team_id: string, subscription_id: string | nu ...acuc, credits_used: acuc.credits_used + credits, adjusted_credits_used: acuc.adjusted_credits_used + credits, - remaining_credits: acuc.remaining_credits - credits, + remaining_credits: acuc.remaining_credits - credits } : null ); @@ -55,21 +67,37 @@ export async function supaBillTeam(team_id: string, subscription_id: string | nu } export type CheckTeamCreditsResponse = { - success: boolean, - message: string, - remainingCredits: number, - chunk?: AuthCreditUsageChunk, -} + success: boolean; + message: string; + remainingCredits: number; + chunk?: AuthCreditUsageChunk; +}; -export async function checkTeamCredits(chunk: AuthCreditUsageChunk | null, team_id: string, credits: number): Promise { - return withAuth(supaCheckTeamCredits, { success: true, message: "No DB, bypassed", remainingCredits: Infinity })(chunk, team_id, credits); +export async function checkTeamCredits( + chunk: AuthCreditUsageChunk | null, + team_id: string, + credits: number +): Promise { + return withAuth(supaCheckTeamCredits, { + success: true, + message: "No DB, bypassed", + remainingCredits: Infinity + })(chunk, team_id, credits); } // if team has enough credits for the operation, return true, else return false -export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk | null, team_id: string, credits: number): Promise { +export async function supaCheckTeamCredits( + chunk: AuthCreditUsageChunk | null, + team_id: string, + credits: number +): Promise { // WARNING: chunk will be null if team_id is preview -- do not perform operations on it under ANY circumstances - mogery if (team_id === "preview") { - return { success: true, message: "Preview team, no credits used", remainingCredits: Infinity }; + return { + success: true, + message: "Preview team, no credits used", + remainingCredits: Infinity + }; } else if (chunk === null) { throw new Error("NULL ACUC passed to supaCheckTeamCredits"); } @@ -81,7 +109,8 @@ export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk | null, t // Removal of + credits const creditUsagePercentage = chunk.adjusted_credits_used / totalPriceCredits; - let isAutoRechargeEnabled = false, autoRechargeThreshold = 1000; + let isAutoRechargeEnabled = false, + autoRechargeThreshold = 1000; const cacheKey = `team_auto_recharge_${team_id}`; let cachedData = await getValue(cacheKey); if (cachedData) { @@ -102,16 +131,19 @@ export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk | null, t } } - if (isAutoRechargeEnabled && chunk.remaining_credits < autoRechargeThreshold) { + if ( + isAutoRechargeEnabled && + chunk.remaining_credits < autoRechargeThreshold + ) { const autoChargeResult = await autoCharge(chunk, autoRechargeThreshold); if (autoChargeResult.success) { return { success: true, - message: autoChargeResult.message, - remainingCredits: autoChargeResult.remainingCredits, - chunk: autoChargeResult.chunk, - }; - } + message: autoChargeResult.message, + remainingCredits: autoChargeResult.remainingCredits, + chunk: autoChargeResult.chunk + }; + } } // Compare the adjusted total credits used with the credits allowed by the plan @@ -131,7 +163,7 @@ export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk | null, t message: "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing.", remainingCredits: chunk.remaining_credits, - chunk, + chunk }; } else if (creditUsagePercentage >= 0.8 && creditUsagePercentage < 1) { // Send email notification for approaching credit limit @@ -148,7 +180,7 @@ export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk | null, t success: true, message: "Sufficient credits available", remainingCredits: chunk.remaining_credits, - chunk, + chunk }; } @@ -202,7 +234,7 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( return { totalCreditsUsed: totalCreditsUsed, remainingCredits, - totalCredits: FREE_CREDITS + couponCredits, + totalCredits: FREE_CREDITS + couponCredits }; } @@ -241,6 +273,6 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( return { totalCreditsUsed, remainingCredits, - totalCredits: price.credits, + totalCredits: price.credits }; } diff --git a/apps/api/src/services/billing/issue_credits.ts b/apps/api/src/services/billing/issue_credits.ts index ce84db1b..3f013a1c 100644 --- a/apps/api/src/services/billing/issue_credits.ts +++ b/apps/api/src/services/billing/issue_credits.ts @@ -8,7 +8,7 @@ export async function issueCredits(team_id: string, credits: number) { credits: credits, status: "active", // indicates that this coupon was issued from auto recharge - from_auto_recharge: true, + from_auto_recharge: true }); if (error) { diff --git a/apps/api/src/services/billing/stripe.ts b/apps/api/src/services/billing/stripe.ts index db482e91..c5b76445 100644 --- a/apps/api/src/services/billing/stripe.ts +++ b/apps/api/src/services/billing/stripe.ts @@ -5,7 +5,7 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? ""); async function getCustomerDefaultPaymentMethod(customerId: string) { const paymentMethods = await stripe.customers.listPaymentMethods(customerId, { - limit: 3, + limit: 3 }); return paymentMethods.data[0] ?? null; } @@ -16,9 +16,12 @@ export async function createPaymentIntent( customer_id: string ): Promise<{ return_status: ReturnStatus; charge_id: string }> { try { - const defaultPaymentMethod = await getCustomerDefaultPaymentMethod(customer_id); + const defaultPaymentMethod = + await getCustomerDefaultPaymentMethod(customer_id); if (!defaultPaymentMethod) { - logger.error(`No default payment method found for customer: ${customer_id}`); + logger.error( + `No default payment method found for customer: ${customer_id}` + ); return { return_status: "failed", charge_id: "" }; } const paymentIntent = await stripe.paymentIntents.create({ @@ -29,7 +32,7 @@ export async function createPaymentIntent( payment_method_types: [defaultPaymentMethod?.type ?? "card"], payment_method: defaultPaymentMethod?.id, off_session: true, - confirm: true, + confirm: true }); if (paymentIntent.status === "succeeded") { diff --git a/apps/api/src/services/idempotency/create.ts b/apps/api/src/services/idempotency/create.ts index f29fc70f..8e1ede44 100644 --- a/apps/api/src/services/idempotency/create.ts +++ b/apps/api/src/services/idempotency/create.ts @@ -2,10 +2,8 @@ import { Request } from "express"; import { supabase_service } from "../supabase"; import { logger } from "../../../src/lib/logger"; -export async function createIdempotencyKey( - req: Request, -): Promise { - const idempotencyKey = req.headers['x-idempotency-key'] as string; +export async function createIdempotencyKey(req: Request): Promise { + const idempotencyKey = req.headers["x-idempotency-key"] as string; if (!idempotencyKey) { throw new Error("No idempotency key provided in the request headers."); } diff --git a/apps/api/src/services/idempotency/validate.ts b/apps/api/src/services/idempotency/validate.ts index ca3acab1..5a347f67 100644 --- a/apps/api/src/services/idempotency/validate.ts +++ b/apps/api/src/services/idempotency/validate.ts @@ -1,28 +1,28 @@ import { Request } from "express"; import { supabase_service } from "../supabase"; -import { validate as isUuid } from 'uuid'; +import { validate as isUuid } from "uuid"; import { logger } from "../../../src/lib/logger"; -export async function validateIdempotencyKey( - req: Request, -): Promise { - const idempotencyKey = req.headers['x-idempotency-key']; +export async function validateIdempotencyKey(req: Request): Promise { + const idempotencyKey = req.headers["x-idempotency-key"]; if (!idempotencyKey) { // // not returning for missing idempotency key for now return true; } - // Ensure idempotencyKey is treated as a string - const key = Array.isArray(idempotencyKey) ? idempotencyKey[0] : idempotencyKey; - if (!isUuid(key)) { - logger.debug("Invalid idempotency key provided in the request headers."); - return false; - } + // Ensure idempotencyKey is treated as a string + const key = Array.isArray(idempotencyKey) + ? idempotencyKey[0] + : idempotencyKey; + if (!isUuid(key)) { + logger.debug("Invalid idempotency key provided in the request headers."); + return false; + } const { data, error } = await supabase_service .from("idempotency_keys") .select("key") .eq("key", idempotencyKey); - + if (error) { logger.error(`Error validating idempotency key: ${error}`); } diff --git a/apps/api/src/services/logging/crawl_log.ts b/apps/api/src/services/logging/crawl_log.ts index 0160828e..bfdc84ce 100644 --- a/apps/api/src/services/logging/crawl_log.ts +++ b/apps/api/src/services/logging/crawl_log.ts @@ -4,17 +4,17 @@ import { configDotenv } from "dotenv"; configDotenv(); export async function logCrawl(job_id: string, team_id: string) { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (useDbAuthentication) { try { const { data, error } = await supabase_service - .from("bulljobs_teams") - .insert([ - { - job_id: job_id, - team_id: team_id, - }, - ]); + .from("bulljobs_teams") + .insert([ + { + job_id: job_id, + team_id: team_id + } + ]); } catch (error) { logger.error(`Error logging crawl job to supabase:\n${error}`); } diff --git a/apps/api/src/services/logging/log_job.ts b/apps/api/src/services/logging/log_job.ts index aaecad25..c3111dd7 100644 --- a/apps/api/src/services/logging/log_job.ts +++ b/apps/api/src/services/logging/log_job.ts @@ -9,7 +9,7 @@ configDotenv(); export async function logJob(job: FirecrawlJob, force: boolean = false) { try { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (!useDbAuthentication) { return; } @@ -21,7 +21,12 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { job.scrapeOptions.headers["Authorization"] ) { job.scrapeOptions.headers["Authorization"] = "REDACTED"; - job.docs = [{ content: "REDACTED DUE TO AUTHORIZATION HEADER", html: "REDACTED DUE TO AUTHORIZATION HEADER" }]; + job.docs = [ + { + content: "REDACTED DUE TO AUTHORIZATION HEADER", + html: "REDACTED DUE TO AUTHORIZATION HEADER" + } + ]; } const jobColumn = { job_id: job.job_id ? job.job_id : null, @@ -38,25 +43,34 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { origin: job.origin, num_tokens: job.num_tokens, retry: !!job.retry, - crawl_id: job.crawl_id, + crawl_id: job.crawl_id }; if (force) { - let i = 0, done = false; + let i = 0, + done = false; while (i++ <= 10) { try { const { error } = await supabase_service .from("firecrawl_jobs") .insert([jobColumn]); if (error) { - logger.error("Failed to log job due to Supabase error -- trying again", { error, scrapeId: job.job_id }); - await new Promise((resolve) => setTimeout(() => resolve(), 75)); + logger.error( + "Failed to log job due to Supabase error -- trying again", + { error, scrapeId: job.job_id } + ); + await new Promise((resolve) => + setTimeout(() => resolve(), 75) + ); } else { done = true; break; } } catch (error) { - logger.error("Failed to log job due to thrown error -- trying again", { error, scrapeId: job.job_id }); + logger.error( + "Failed to log job due to thrown error -- trying again", + { error, scrapeId: job.job_id } + ); await new Promise((resolve) => setTimeout(() => resolve(), 75)); } } @@ -70,7 +84,10 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { .from("firecrawl_jobs") .insert([jobColumn]); if (error) { - logger.error(`Error logging job: ${error.message}`, { error, scrapeId: job.job_id }); + logger.error(`Error logging job: ${error.message}`, { + error, + scrapeId: job.job_id + }); } else { logger.debug("Job logged successfully!", { scrapeId: job.job_id }); } @@ -80,7 +97,7 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { let phLog = { distinctId: "from-api", //* To identify this on the group level, setting distinctid to a static string per posthog docs: https://posthog.com/docs/product-analytics/group-analytics#advanced-server-side-only-capturing-group-events-without-a-user ...(job.team_id !== "preview" && { - groups: { team: job.team_id }, + groups: { team: job.team_id } }), //* Identifying event on this team event: "job-logged", properties: { @@ -95,14 +112,13 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { page_options: job.scrapeOptions, origin: job.origin, num_tokens: job.num_tokens, - retry: job.retry, - }, + retry: job.retry + } }; - if(job.mode !== "single_urls") { + if (job.mode !== "single_urls") { posthog.capture(phLog); } } - } catch (error) { logger.error(`Error logging job: ${error.message}`); } diff --git a/apps/api/src/services/logging/scrape_log.ts b/apps/api/src/services/logging/scrape_log.ts index 441b3894..3ccaf777 100644 --- a/apps/api/src/services/logging/scrape_log.ts +++ b/apps/api/src/services/logging/scrape_log.ts @@ -10,7 +10,7 @@ export async function logScrape( scrapeLog: ScrapeLog, pageOptions?: PageOptions ) { - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (!useDbAuthentication) { logger.debug("Skipping logging scrape to Supabase"); return; @@ -42,8 +42,8 @@ export async function logScrape( date_added: new Date().toISOString(), html: "Removed to save db space", ipv4_support: scrapeLog.ipv4_support, - ipv6_support: scrapeLog.ipv6_support, - }, + ipv6_support: scrapeLog.ipv6_support + } ]); if (error) { diff --git a/apps/api/src/services/notification/email_notification.ts b/apps/api/src/services/notification/email_notification.ts index 982d402e..22c23865 100644 --- a/apps/api/src/services/notification/email_notification.ts +++ b/apps/api/src/services/notification/email_notification.ts @@ -14,25 +14,25 @@ const emailTemplates: Record< > = { [NotificationType.APPROACHING_LIMIT]: { subject: "You've used 80% of your credit limit - Firecrawl", - html: "Hey there,

Thanks,
Firecrawl Team
", + html: "Hey there,

You are approaching your credit limit for this billing period. Your usage right now is around 80% of your total credit limit. Consider upgrading your plan to avoid hitting the limit. Check out our pricing page for more info.


Thanks,
Firecrawl Team
" }, [NotificationType.LIMIT_REACHED]: { subject: "Credit Limit Reached! Take action now to resume usage - Firecrawl", - html: "Hey there,

You have reached your credit limit for this billing period. To resume usage, please upgrade your plan. Check out our pricing page for more info.


Thanks,
Firecrawl Team
", + html: "Hey there,

You have reached your credit limit for this billing period. To resume usage, please upgrade your plan. Check out our pricing page for more info.


Thanks,
Firecrawl Team
" }, [NotificationType.RATE_LIMIT_REACHED]: { subject: "Rate Limit Reached - Firecrawl", - html: "Hey there,

You've hit one of the Firecrawl endpoint's rate limit! Take a breather and try again in a few moments. If you need higher rate limits, consider upgrading your plan. Check out our pricing page for more info.

If you have any questions, feel free to reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team

Ps. this email is only sent once every 7 days if you reach a rate limit.", + html: "Hey there,

You've hit one of the Firecrawl endpoint's rate limit! Take a breather and try again in a few moments. If you need higher rate limits, consider upgrading your plan. Check out our pricing page for more info.

If you have any questions, feel free to reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team

Ps. this email is only sent once every 7 days if you reach a rate limit." }, [NotificationType.AUTO_RECHARGE_SUCCESS]: { subject: "Auto recharge successful - Firecrawl", - html: "Hey there,

Your account was successfully recharged with 1000 credits because your remaining credits were below the threshold. Consider upgrading your plan at firecrawl.dev/pricing to avoid hitting the limit.


Thanks,
Firecrawl Team
", + html: "Hey there,

Your account was successfully recharged with 1000 credits because your remaining credits were below the threshold. Consider upgrading your plan at firecrawl.dev/pricing to avoid hitting the limit.


Thanks,
Firecrawl Team
" }, [NotificationType.AUTO_RECHARGE_FAILED]: { subject: "Auto recharge failed - Firecrawl", - html: "Hey there,

Your auto recharge failed. Please try again manually. If the issue persists, please reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team
", - }, + html: "Hey there,

Your auto recharge failed. Please try again manually. If the issue persists, please reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team
" + } }; export async function sendNotification( @@ -55,7 +55,7 @@ export async function sendNotification( export async function sendEmailNotification( email: string, - notificationType: NotificationType, + notificationType: NotificationType ) { const resend = new Resend(process.env.RESEND_API_KEY); @@ -65,7 +65,7 @@ export async function sendEmailNotification( to: [email], reply_to: "help@firecrawl.com", subject: emailTemplates[notificationType].subject, - html: emailTemplates[notificationType].html, + html: emailTemplates[notificationType].html }); if (error) { @@ -89,91 +89,97 @@ export async function sendNotificationInternal( if (team_id === "preview") { return { success: true }; } - return await redlock.using([`notification-lock:${team_id}:${notificationType}`], 5000, async () => { + return await redlock.using( + [`notification-lock:${team_id}:${notificationType}`], + 5000, + async () => { + if (!bypassRecentChecks) { + const fifteenDaysAgo = new Date(); + fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15); - if (!bypassRecentChecks) { - const fifteenDaysAgo = new Date(); - fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15); + const { data, error } = await supabase_service + .from("user_notifications") + .select("*") + .eq("team_id", team_id) + .eq("notification_type", notificationType) + .gte("sent_date", fifteenDaysAgo.toISOString()); - const { data, error } = await supabase_service - .from("user_notifications") - .select("*") - .eq("team_id", team_id) - .eq("notification_type", notificationType) - .gte("sent_date", fifteenDaysAgo.toISOString()); + if (error) { + logger.debug(`Error fetching notifications: ${error}`); + return { success: false }; + } - if (error) { - logger.debug(`Error fetching notifications: ${error}`); - return { success: false }; + if (data.length !== 0) { + return { success: false }; + } + + // TODO: observation: Free credits people are not receiving notifications + + const { data: recentData, error: recentError } = await supabase_service + .from("user_notifications") + .select("*") + .eq("team_id", team_id) + .eq("notification_type", notificationType) + .gte("sent_date", startDateString) + .lte("sent_date", endDateString); + + if (recentError) { + logger.debug( + `Error fetching recent notifications: ${recentError.message}` + ); + return { success: false }; + } + + if (recentData.length !== 0) { + return { success: false }; + } + } + + console.log( + `Sending notification for team_id: ${team_id} and notificationType: ${notificationType}` + ); + // get the emails from the user with the team_id + const { data: emails, error: emailsError } = await supabase_service + .from("users") + .select("email") + .eq("team_id", team_id); + + if (emailsError) { + logger.debug(`Error fetching emails: ${emailsError}`); + return { success: false }; + } + + for (const email of emails) { + await sendEmailNotification(email.email, notificationType); + } + + const { error: insertError } = await supabase_service + .from("user_notifications") + .insert([ + { + team_id: team_id, + notification_type: notificationType, + sent_date: new Date().toISOString(), + timestamp: new Date().toISOString() + } + ]); + + if (process.env.SLACK_ADMIN_WEBHOOK_URL && emails.length > 0) { + sendSlackWebhook( + `${getNotificationString(notificationType)}: Team ${team_id}, with email ${emails[0].email}. Number of credits used: ${chunk.adjusted_credits_used} | Number of credits in the plan: ${chunk.price_credits}`, + false, + process.env.SLACK_ADMIN_WEBHOOK_URL + ).catch((error) => { + logger.debug(`Error sending slack notification: ${error}`); + }); + } + + if (insertError) { + logger.debug(`Error inserting notification record: ${insertError}`); + return { success: false }; + } + + return { success: true }; } - - if (data.length !== 0) { - return { success: false }; - } - - // TODO: observation: Free credits people are not receiving notifications - - const { data: recentData, error: recentError } = await supabase_service - .from("user_notifications") - .select("*") - .eq("team_id", team_id) - .eq("notification_type", notificationType) - .gte("sent_date", startDateString) - .lte("sent_date", endDateString); - - if (recentError) { - logger.debug(`Error fetching recent notifications: ${recentError.message}`); - return { success: false }; - } - - if (recentData.length !== 0) { - return { success: false }; - } - - } - - console.log(`Sending notification for team_id: ${team_id} and notificationType: ${notificationType}`); - // get the emails from the user with the team_id - const { data: emails, error: emailsError } = await supabase_service - .from("users") - .select("email") - .eq("team_id", team_id); - - if (emailsError) { - logger.debug(`Error fetching emails: ${emailsError}`); - return { success: false }; - } - - for (const email of emails) { - await sendEmailNotification(email.email, notificationType); - } - - const { error: insertError } = await supabase_service - .from("user_notifications") - .insert([ - { - team_id: team_id, - notification_type: notificationType, - sent_date: new Date().toISOString(), - timestamp: new Date().toISOString(), - }, - ]); - - if (process.env.SLACK_ADMIN_WEBHOOK_URL && emails.length > 0) { - sendSlackWebhook( - `${getNotificationString(notificationType)}: Team ${team_id}, with email ${emails[0].email}. Number of credits used: ${chunk.adjusted_credits_used} | Number of credits in the plan: ${chunk.price_credits}`, - false, - process.env.SLACK_ADMIN_WEBHOOK_URL - ).catch((error) => { - logger.debug(`Error sending slack notification: ${error}`); - }); - } - - if (insertError) { - logger.debug(`Error inserting notification record: ${insertError}`); - return { success: false }; - } - - return { success: true }; - }); + ); } diff --git a/apps/api/src/services/posthog.ts b/apps/api/src/services/posthog.ts index e3a01353..69f370ec 100644 --- a/apps/api/src/services/posthog.ts +++ b/apps/api/src/services/posthog.ts @@ -1,6 +1,6 @@ -import { PostHog } from 'posthog-node'; +import { PostHog } from "posthog-node"; import "dotenv/config"; -import { logger } from '../../src/lib/logger'; +import { logger } from "../../src/lib/logger"; export default function PostHogClient(apiKey: string) { const posthogClient = new PostHog(apiKey, { @@ -24,4 +24,4 @@ export const posthog = process.env.POSTHOG_API_KEY "POSTHOG_API_KEY is not provided - your events will not be logged. Using MockPostHog as a fallback. See posthog.ts for more." ); return new MockPostHog(); - })(); \ No newline at end of file + })(); diff --git a/apps/api/src/services/queue-jobs.ts b/apps/api/src/services/queue-jobs.ts index bc2debfe..b4bd799b 100644 --- a/apps/api/src/services/queue-jobs.ts +++ b/apps/api/src/services/queue-jobs.ts @@ -3,7 +3,13 @@ import { getScrapeQueue } from "./queue-service"; import { v4 as uuidv4 } from "uuid"; import { WebScraperOptions } from "../types"; import * as Sentry from "@sentry/node"; -import { cleanOldConcurrencyLimitEntries, getConcurrencyLimitActiveJobs, getConcurrencyLimitMax, pushConcurrencyLimitActiveJob, pushConcurrencyLimitedJob } from "../lib/concurrency-limit"; +import { + cleanOldConcurrencyLimitEntries, + getConcurrencyLimitActiveJobs, + getConcurrencyLimitMax, + pushConcurrencyLimitActiveJob, + pushConcurrencyLimitedJob +} from "../lib/concurrency-limit"; async function addScrapeJobRaw( webScraperOptions: any, @@ -13,11 +19,17 @@ async function addScrapeJobRaw( ) { let concurrencyLimited = false; - if (webScraperOptions && webScraperOptions.team_id && webScraperOptions.plan) { + if ( + webScraperOptions && + webScraperOptions.team_id && + webScraperOptions.plan + ) { const now = Date.now(); const limit = await getConcurrencyLimitMax(webScraperOptions.plan); cleanOldConcurrencyLimitEntries(webScraperOptions.team_id, now); - concurrencyLimited = (await getConcurrencyLimitActiveJobs(webScraperOptions.team_id, now)).length >= limit; + concurrencyLimited = + (await getConcurrencyLimitActiveJobs(webScraperOptions.team_id, now)) + .length >= limit; } if (concurrencyLimited) { @@ -27,19 +39,23 @@ async function addScrapeJobRaw( opts: { ...options, priority: jobPriority, - jobId: jobId, + jobId: jobId }, - priority: jobPriority, + priority: jobPriority }); } else { - if (webScraperOptions && webScraperOptions.team_id && webScraperOptions.plan) { + if ( + webScraperOptions && + webScraperOptions.team_id && + webScraperOptions.plan + ) { await pushConcurrencyLimitActiveJob(webScraperOptions.team_id, jobId); } await getScrapeQueue().add(jobId, webScraperOptions, { ...options, priority: jobPriority, - jobId, + jobId }); } } @@ -52,24 +68,32 @@ export async function addScrapeJob( ) { if (Sentry.isInitialized()) { const size = JSON.stringify(webScraperOptions).length; - return await Sentry.startSpan({ - name: "Add scrape job", - op: "queue.publish", - attributes: { - "messaging.message.id": jobId, - "messaging.destination.name": getScrapeQueue().name, - "messaging.message.body.size": size, + return await Sentry.startSpan( + { + name: "Add scrape job", + op: "queue.publish", + attributes: { + "messaging.message.id": jobId, + "messaging.destination.name": getScrapeQueue().name, + "messaging.message.body.size": size + } }, - }, async (span) => { - await addScrapeJobRaw({ - ...webScraperOptions, - sentry: { - trace: Sentry.spanToTraceHeader(span), - baggage: Sentry.spanToBaggageHeader(span), - size, - }, - }, options, jobId, jobPriority); - }); + async (span) => { + await addScrapeJobRaw( + { + ...webScraperOptions, + sentry: { + trace: Sentry.spanToTraceHeader(span), + baggage: Sentry.spanToBaggageHeader(span), + size + } + }, + options, + jobId, + jobPriority + ); + } + ); } else { await addScrapeJobRaw(webScraperOptions, options, jobId, jobPriority); } @@ -77,18 +101,25 @@ export async function addScrapeJob( export async function addScrapeJobs( jobs: { - data: WebScraperOptions, + data: WebScraperOptions; opts: { - jobId: string, - priority: number, - }, - }[], + jobId: string; + priority: number; + }; + }[] ) { // TODO: better - await Promise.all(jobs.map(job => addScrapeJob(job.data, job.opts, job.opts.jobId, job.opts.priority))); + await Promise.all( + jobs.map((job) => + addScrapeJob(job.data, job.opts, job.opts.jobId, job.opts.priority) + ) + ); } -export function waitForJob(jobId: string, timeout: number): Promise { +export function waitForJob( + jobId: string, + timeout: number +): Promise { return new Promise((resolve, reject) => { const start = Date.now(); const int = setInterval(async () => { @@ -110,5 +141,5 @@ export function waitForJob(jobId: string, timeout: number): Promise } } }, 250); - }) + }); } diff --git a/apps/api/src/services/queue-service.ts b/apps/api/src/services/queue-service.ts index e6432a3f..3970a6e7 100644 --- a/apps/api/src/services/queue-service.ts +++ b/apps/api/src/services/queue-service.ts @@ -5,7 +5,7 @@ import IORedis from "ioredis"; let scrapeQueue: Queue; export const redisConnection = new IORedis(process.env.REDIS_URL!, { - maxRetriesPerRequest: null, + maxRetriesPerRequest: null }); export const scrapeQueueName = "{scrapeQueue}"; @@ -18,12 +18,12 @@ export function getScrapeQueue() { connection: redisConnection, defaultJobOptions: { removeOnComplete: { - age: 90000, // 25 hours + age: 90000 // 25 hours }, removeOnFail: { - age: 90000, // 25 hours - }, - }, + age: 90000 // 25 hours + } + } } // { // settings: { @@ -42,7 +42,6 @@ export function getScrapeQueue() { return scrapeQueue; } - // === REMOVED IN FAVOR OF POLLING -- NOT RELIABLE // import { QueueEvents } from 'bullmq'; -// export const scrapeQueueEvents = new QueueEvents(scrapeQueueName, { connection: redisConnection.duplicate() }); \ No newline at end of file +// export const scrapeQueueEvents = new QueueEvents(scrapeQueueName, { connection: redisConnection.duplicate() }); diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index 74e954cd..dc352d36 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -5,7 +5,7 @@ import { CustomError } from "../lib/custom-error"; import { getScrapeQueue, redisConnection, - scrapeQueueName, + scrapeQueueName } from "./queue-service"; import { startWebScraperPipeline } from "../main/runWebScraper"; import { callWebhook } from "./webhook"; @@ -24,26 +24,31 @@ import { getCrawl, getCrawlJobs, lockURL, - normalizeURL, + normalizeURL } from "../lib/crawl-redis"; import { StoredCrawl } from "../lib/crawl-redis"; import { addScrapeJob } from "./queue-jobs"; import { addJobPriority, deleteJobPriority, - getJobPriority, + getJobPriority } from "../../src/lib/job-priority"; import { PlanType, RateLimiterMode } from "../types"; import { getJobs } from "..//controllers/v1/crawl-status"; import { configDotenv } from "dotenv"; import { scrapeOptions } from "../controllers/v1/types"; import { getRateLimiterPoints } from "./rate-limiter"; -import { cleanOldConcurrencyLimitEntries, pushConcurrencyLimitActiveJob, removeConcurrencyLimitActiveJob, takeConcurrencyLimitedJob } from "../lib/concurrency-limit"; +import { + cleanOldConcurrencyLimitEntries, + pushConcurrencyLimitActiveJob, + removeConcurrencyLimitActiveJob, + takeConcurrencyLimitedJob +} from "../lib/concurrency-limit"; configDotenv(); class RacedRedirectError extends Error { constructor() { - super("Raced redirect error") + super("Raced redirect error"); } } @@ -63,21 +68,28 @@ const connectionMonitorInterval = Number(process.env.CONNECTION_MONITOR_INTERVAL) || 10; const gotJobInterval = Number(process.env.CONNECTION_MONITOR_INTERVAL) || 20; -async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { +async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { if (await finishCrawl(job.data.crawl_id)) { if (!job.data.v1) { const jobIDs = await getCrawlJobs(job.data.crawl_id); - const jobs = (await getJobs(jobIDs)).sort((a, b) => a.timestamp - b.timestamp); + const jobs = (await getJobs(jobIDs)).sort( + (a, b) => a.timestamp - b.timestamp + ); // const jobStatuses = await Promise.all(jobs.map((x) => x.getState())); - const jobStatus = - sc.cancelled // || jobStatuses.some((x) => x === "failed") - ? "failed" - : "completed"; + const jobStatus = sc.cancelled // || jobStatuses.some((x) => x === "failed") + ? "failed" + : "completed"; - const fullDocs = jobs.map((x) => - x.returnvalue ? (Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue) : null - ).filter(x => x !== null); + const fullDocs = jobs + .map((x) => + x.returnvalue + ? Array.isArray(x.returnvalue) + ? x.returnvalue[0] + : x.returnvalue + : null + ) + .filter((x) => x !== null); await logJob({ job_id: job.data.crawl_id, @@ -91,7 +103,7 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { url: sc.originUrl!, scrapeOptions: sc.scrapeOptions, crawlerOptions: sc.crawlerOptions, - origin: job.data.origin, + origin: job.data.origin }); const data = { @@ -100,12 +112,12 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { links: fullDocs.map((doc) => { return { content: doc, - source: doc?.metadata?.sourceURL ?? doc?.url ?? "", + source: doc?.metadata?.sourceURL ?? doc?.url ?? "" }; - }), + }) }, project_id: job.data.project_id, - docs: fullDocs, + docs: fullDocs }; // v0 web hooks, call when done with all the data @@ -116,15 +128,14 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { data, job.data.webhook, job.data.v1, - job.data.crawlerOptions !== null ? "crawl.completed" : "batch_scrape.completed" + job.data.crawlerOptions !== null + ? "crawl.completed" + : "batch_scrape.completed" ); } } else { const jobIDs = await getCrawlJobs(job.data.crawl_id); - const jobStatus = - sc.cancelled - ? "failed" - : "completed"; + const jobStatus = sc.cancelled ? "failed" : "completed"; // v1 web hooks, call when done with no data, but with event completed if (job.data.v1 && job.data.webhook) { @@ -134,30 +145,43 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { [], job.data.webhook, job.data.v1, - job.data.crawlerOptions !== null ? "crawl.completed" : "batch_scrape.completed" - ); - } + job.data.crawlerOptions !== null + ? "crawl.completed" + : "batch_scrape.completed" + ); + } - await logJob({ - job_id: job.data.crawl_id, - success: jobStatus === "completed", - message: sc.cancelled ? "Cancelled" : undefined, - num_docs: jobIDs.length, - docs: [], - time_taken: (Date.now() - sc.createdAt) / 1000, - team_id: job.data.team_id, - scrapeOptions: sc.scrapeOptions, - mode: job.data.crawlerOptions !== null ? "crawl" : "batch_scrape", - url: sc?.originUrl ?? (job.data.crawlerOptions === null ? "Batch Scrape" : "Unknown"), - crawlerOptions: sc.crawlerOptions, - origin: job.data.origin, - }, true); + await logJob( + { + job_id: job.data.crawl_id, + success: jobStatus === "completed", + message: sc.cancelled ? "Cancelled" : undefined, + num_docs: jobIDs.length, + docs: [], + time_taken: (Date.now() - sc.createdAt) / 1000, + team_id: job.data.team_id, + scrapeOptions: sc.scrapeOptions, + mode: job.data.crawlerOptions !== null ? "crawl" : "batch_scrape", + url: + sc?.originUrl ?? + (job.data.crawlerOptions === null ? "Batch Scrape" : "Unknown"), + crawlerOptions: sc.crawlerOptions, + origin: job.data.origin + }, + true + ); } } } const processJobInternal = async (token: string, job: Job & { id: string }) => { - const logger = _logger.child({ module: "queue-worker", method: "processJobInternal", jobId: job.id, scrapeId: job.id, crawlId: job.data?.crawl_id ?? undefined }); + const logger = _logger.child({ + module: "queue-worker", + method: "processJobInternal", + jobId: job.id, + scrapeId: job.id, + crawlId: job.data?.crawl_id ?? undefined + }); const extendLockInterval = setInterval(async () => { logger.info(`🐂 Worker extending lock on job ${job.id}`); @@ -171,7 +195,9 @@ const processJobInternal = async (token: string, job: Job & { id: string }) => { if (result.success) { try { if (job.data.crawl_id && process.env.USE_DB_AUTHENTICATION === "true") { - logger.debug("Job succeeded -- has crawl associated, putting null in Redis"); + logger.debug( + "Job succeeded -- has crawl associated, putting null in Redis" + ); await job.moveToCompleted(null, token, false); } else { logger.debug("Job succeeded -- putting result in Redis"); @@ -220,7 +246,7 @@ const workerFun = async ( lockDuration: 1 * 60 * 1000, // 1 minute // lockRenewTime: 15 * 1000, // 15 seconds stalledInterval: 30 * 1000, // 30 seconds - maxStalledCount: 10, // 10 times + maxStalledCount: 10 // 10 times }); worker.startStalledCheckTimer(); @@ -241,7 +267,7 @@ const workerFun = async ( if (cantAcceptConnectionCount >= 25) { logger.error("WORKER STALLED", { cpuUsage: await monitor.checkCpuUsage(), - memoryUsage: await monitor.checkMemoryUsage(), + memoryUsage: await monitor.checkMemoryUsage() }); } @@ -265,14 +291,18 @@ const workerFun = async ( if (nextJob !== null) { await pushConcurrencyLimitActiveJob(job.data.team_id, nextJob.id); - await queue.add(nextJob.id, { - ...nextJob.data, - concurrencyLimitHit: true, - }, { - ...nextJob.opts, - jobId: nextJob.id, - priority: nextJob.priority, - }); + await queue.add( + nextJob.id, + { + ...nextJob.data, + concurrencyLimitHit: true + }, + { + ...nextJob.opts, + jobId: nextJob.id, + priority: nextJob.priority + } + ); } } } @@ -281,7 +311,7 @@ const workerFun = async ( Sentry.continueTrace( { sentryTrace: job.data.sentry.trace, - baggage: job.data.sentry.baggage, + baggage: job.data.sentry.baggage }, () => { Sentry.startSpan( @@ -289,8 +319,8 @@ const workerFun = async ( name: "Scrape job", attributes: { job: job.id, - worker: process.env.FLY_MACHINE_ID ?? worker.id, - }, + worker: process.env.FLY_MACHINE_ID ?? worker.id + } }, async (span) => { await Sentry.startSpan( @@ -303,17 +333,17 @@ const workerFun = async ( "messaging.message.body.size": job.data.sentry.size, "messaging.message.receive.latency": Date.now() - (job.processedOn ?? job.timestamp), - "messaging.message.retry.count": job.attemptsMade, - }, + "messaging.message.retry.count": job.attemptsMade + } }, async () => { let res; try { res = await processJobInternal(token, job); - } finally { - await afterJobDone(job) + } finally { + await afterJobDone(job); } - + if (res !== null) { span.setStatus({ code: 2 }); // ERROR } else { @@ -331,12 +361,11 @@ const workerFun = async ( name: "Scrape job", attributes: { job: job.id, - worker: process.env.FLY_MACHINE_ID ?? worker.id, - }, + worker: process.env.FLY_MACHINE_ID ?? worker.id + } }, () => { - processJobInternal(token, job) - .finally(() => afterJobDone(job)); + processJobInternal(token, job).finally(() => afterJobDone(job)); } ); } @@ -351,7 +380,13 @@ const workerFun = async ( workerFun(getScrapeQueue(), processJobInternal); async function processJob(job: Job & { id: string }, token: string) { - const logger = _logger.child({ module: "queue-worker", method: "processJob", jobId: job.id, scrapeId: job.id, crawlId: job.data?.crawl_id ?? undefined }); + const logger = _logger.child({ + module: "queue-worker", + method: "processJob", + jobId: job.id, + scrapeId: job.id, + crawlId: job.data?.crawl_id ?? undefined + }); logger.info(`🐂 Worker taking job ${job.id}`, { url: job.data.url }); // Check if the job URL is researchhub and block it immediately @@ -368,7 +403,7 @@ async function processJob(job: Job & { id: string }, token: string) { document: null, project_id: job.data.project_id, error: - "URL is blocked. Suspecious activity detected. Please contact help@firecrawl.com if you believe this is an error.", + "URL is blocked. Suspecious activity detected. Please contact help@firecrawl.com if you believe this is an error." }; return data; } @@ -378,21 +413,23 @@ async function processJob(job: Job & { id: string }, token: string) { current: 1, total: 100, current_step: "SCRAPING", - current_url: "", + current_url: "" }); const start = Date.now(); const pipeline = await Promise.race([ startWebScraperPipeline({ job, - token, + token }), - ...(job.data.scrapeOptions.timeout !== undefined ? [ - (async () => { - await sleep(job.data.scrapeOptions.timeout); - throw new Error("timeout") - })(), - ] : []) + ...(job.data.scrapeOptions.timeout !== undefined + ? [ + (async () => { + await sleep(job.data.scrapeOptions.timeout); + throw new Error("timeout"); + })() + ] + : []) ]); if (!pipeline.success) { @@ -410,17 +447,21 @@ async function processJob(job: Job & { id: string }, token: string) { const data = { success: true, result: { - links: [{ - content: doc, - source: doc?.metadata?.sourceURL ?? doc?.metadata?.url ?? "", - }], + links: [ + { + content: doc, + source: doc?.metadata?.sourceURL ?? doc?.metadata?.url ?? "" + } + ] }, project_id: job.data.project_id, - document: doc, + document: doc }; if (job.data.webhook && job.data.mode !== "crawl" && job.data.v1) { - logger.debug("Calling webhook with success...", { webhook: job.data.webhook }); + logger.debug("Calling webhook with success...", { + webhook: job.data.webhook + }); await callWebhook( job.data.team_id, job.data.crawl_id, @@ -434,54 +475,83 @@ async function processJob(job: Job & { id: string }, token: string) { if (job.data.crawl_id) { const sc = (await getCrawl(job.data.crawl_id)) as StoredCrawl; - - if (doc.metadata.url !== undefined && doc.metadata.sourceURL !== undefined && normalizeURL(doc.metadata.url, sc) !== normalizeURL(doc.metadata.sourceURL, sc)) { - logger.debug("Was redirected, removing old URL and locking new URL...", { oldUrl: doc.metadata.sourceURL, newUrl: doc.metadata.url }); + + if ( + doc.metadata.url !== undefined && + doc.metadata.sourceURL !== undefined && + normalizeURL(doc.metadata.url, sc) !== + normalizeURL(doc.metadata.sourceURL, sc) + ) { + logger.debug( + "Was redirected, removing old URL and locking new URL...", + { oldUrl: doc.metadata.sourceURL, newUrl: doc.metadata.url } + ); // Remove the old URL from visited unique due to checking for limit // Do not remove from :visited otherwise it will keep crawling the original URL (sourceURL) - await redisConnection.srem("crawl:" + job.data.crawl_id + ":visited_unique", normalizeURL(doc.metadata.sourceURL, sc)); + await redisConnection.srem( + "crawl:" + job.data.crawl_id + ":visited_unique", + normalizeURL(doc.metadata.sourceURL, sc) + ); const p1 = generateURLPermutations(normalizeURL(doc.metadata.url, sc)); - const p2 = generateURLPermutations(normalizeURL(doc.metadata.sourceURL, sc)); + const p2 = generateURLPermutations( + normalizeURL(doc.metadata.sourceURL, sc) + ); // In crawls, we should only crawl a redirected page once, no matter how many; times it is redirected to, or if it's been discovered by the crawler before. // This can prevent flakiness with race conditions. // Lock the new URL const lockRes = await lockURL(job.data.crawl_id, sc, doc.metadata.url); - if (job.data.crawlerOptions !== null && !lockRes && JSON.stringify(p1) !== JSON.stringify(p2)) { + if ( + job.data.crawlerOptions !== null && + !lockRes && + JSON.stringify(p1) !== JSON.stringify(p2) + ) { throw new RacedRedirectError(); } } logger.debug("Logging job to DB..."); - await logJob({ - job_id: job.id as string, - success: true, - num_docs: 1, - docs: [doc], - time_taken: timeTakenInSeconds, - team_id: job.data.team_id, - mode: job.data.mode, - url: job.data.url, - crawlerOptions: sc.crawlerOptions, - scrapeOptions: job.data.scrapeOptions, - origin: job.data.origin, - crawl_id: job.data.crawl_id, - }, true); + await logJob( + { + job_id: job.id as string, + success: true, + num_docs: 1, + docs: [doc], + time_taken: timeTakenInSeconds, + team_id: job.data.team_id, + mode: job.data.mode, + url: job.data.url, + crawlerOptions: sc.crawlerOptions, + scrapeOptions: job.data.scrapeOptions, + origin: job.data.origin, + crawl_id: job.data.crawl_id + }, + true + ); logger.debug("Declaring job as done..."); await addCrawlJobDone(job.data.crawl_id, job.id, true); if (job.data.crawlerOptions !== null) { if (!sc.cancelled) { - const crawler = crawlToCrawler(job.data.crawl_id, sc, doc.metadata.url ?? doc.metadata.sourceURL ?? sc.originUrl!); + const crawler = crawlToCrawler( + job.data.crawl_id, + sc, + doc.metadata.url ?? doc.metadata.sourceURL ?? sc.originUrl! + ); const links = crawler.filterLinks( - crawler.extractLinksFromHTML(rawHtml ?? "", doc.metadata?.url ?? doc.metadata?.sourceURL ?? sc.originUrl!), + crawler.extractLinksFromHTML( + rawHtml ?? "", + doc.metadata?.url ?? doc.metadata?.sourceURL ?? sc.originUrl! + ), Infinity, sc.crawlerOptions?.maxDepth ?? 10 ); - logger.debug("Discovered " + links.length + " links...", { linksLength: links.length }); + logger.debug("Discovered " + links.length + " links...", { + linksLength: links.length + }); for (const link of links) { if (await lockURL(job.data.crawl_id, sc, link)) { @@ -489,11 +559,17 @@ async function processJob(job: Job & { id: string }, token: string) { const jobPriority = await getJobPriority({ plan: sc.plan as PlanType, team_id: sc.team_id, - basePriority: job.data.crawl_id ? 20 : 10, + basePriority: job.data.crawl_id ? 20 : 10 }); const jobId = uuidv4(); - logger.debug("Determined job priority " + jobPriority + " for URL " + JSON.stringify(link), { jobPriority, url: link }); + logger.debug( + "Determined job priority " + + jobPriority + + " for URL " + + JSON.stringify(link), + { jobPriority, url: link } + ); // console.log("plan: ", sc.plan); // console.log("team_id: ", sc.team_id) @@ -511,7 +587,7 @@ async function processJob(job: Job & { id: string }, token: string) { origin: job.data.origin, crawl_id: job.data.crawl_id, webhook: job.data.webhook, - v1: job.data.v1, + v1: job.data.v1 }, {}, jobId, @@ -519,9 +595,15 @@ async function processJob(job: Job & { id: string }, token: string) { ); await addCrawlJob(job.data.crawl_id, jobId); - logger.debug("Added job for URL " + JSON.stringify(link), { jobPriority, url: link, newJobId: jobId }); + logger.debug("Added job for URL " + JSON.stringify(link), { + jobPriority, + url: link, + newJobId: jobId + }); } else { - logger.debug("Could not lock URL " + JSON.stringify(link), { url: link }); + logger.debug("Could not lock URL " + JSON.stringify(link), { + url: link + }); } } } @@ -533,7 +615,8 @@ async function processJob(job: Job & { id: string }, token: string) { logger.info(`🐂 Job done ${job.id}`); return data; } catch (error) { - const isEarlyTimeout = error instanceof Error && error.message === "timeout"; + const isEarlyTimeout = + error instanceof Error && error.message === "timeout"; if (isEarlyTimeout) { logger.error(`🐂 Job timed out ${job.id}`); @@ -544,8 +627,8 @@ async function processJob(job: Job & { id: string }, token: string) { Sentry.captureException(error, { data: { - job: job.id, - }, + job: job.id + } }); if (error instanceof CustomError) { @@ -562,7 +645,12 @@ async function processJob(job: Job & { id: string }, token: string) { success: false, document: null, project_id: job.data.project_id, - error: error instanceof Error ? error : typeof error === "string" ? new Error(error) : new Error(JSON.stringify(error)), + error: + error instanceof Error + ? error + : typeof error === "string" + ? new Error(error) + : new Error(JSON.stringify(error)) }; if (!job.data.v1 && (job.data.mode === "crawl" || job.data.crawl_id)) { @@ -572,7 +660,7 @@ async function processJob(job: Job & { id: string }, token: string) { data, job.data.webhook, job.data.v1, - job.data.crawlerOptions !== null ? "crawl.page" : "batch_scrape.page", + job.data.crawlerOptions !== null ? "crawl.page" : "batch_scrape.page" ); } // if (job.data.v1) { @@ -588,31 +676,34 @@ async function processJob(job: Job & { id: string }, token: string) { if (job.data.crawl_id) { const sc = (await getCrawl(job.data.crawl_id)) as StoredCrawl; - + logger.debug("Declaring job as done..."); await addCrawlJobDone(job.data.crawl_id, job.id, false); logger.debug("Logging job to DB..."); - await logJob({ - job_id: job.id as string, - success: false, - message: - typeof error === "string" - ? error - : error.message ?? - "Something went wrong... Contact help@mendable.ai", - num_docs: 0, - docs: [], - time_taken: 0, - team_id: job.data.team_id, - mode: job.data.mode, - url: job.data.url, - crawlerOptions: sc.crawlerOptions, - scrapeOptions: job.data.scrapeOptions, - origin: job.data.origin, - crawl_id: job.data.crawl_id, - }, true); - + await logJob( + { + job_id: job.id as string, + success: false, + message: + typeof error === "string" + ? error + : (error.message ?? + "Something went wrong... Contact help@mendable.ai"), + num_docs: 0, + docs: [], + time_taken: 0, + team_id: job.data.team_id, + mode: job.data.mode, + url: job.data.url, + crawlerOptions: sc.crawlerOptions, + scrapeOptions: job.data.scrapeOptions, + origin: job.data.origin, + crawl_id: job.data.crawl_id + }, + true + ); + await finishCrawlIfNeeded(job, sc); // await logJob({ diff --git a/apps/api/src/services/rate-limiter.test.ts b/apps/api/src/services/rate-limiter.test.ts index 4052bfff..5c25a8d7 100644 --- a/apps/api/src/services/rate-limiter.test.ts +++ b/apps/api/src/services/rate-limiter.test.ts @@ -2,7 +2,7 @@ import { getRateLimiter, serverRateLimiter, testSuiteRateLimiter, - redisRateLimitClient, + redisRateLimitClient } from "./rate-limiter"; import { RateLimiterMode } from "../../src/types"; import { RateLimiterRedis } from "rate-limiter-flexible"; @@ -25,7 +25,7 @@ describe("Rate Limiter Service", () => { afterAll(async () => { try { // if (process.env.REDIS_RATE_LIMIT_URL === "redis://localhost:6379") { - await redisRateLimitClient.disconnect(); + await redisRateLimitClient.disconnect(); // } } catch (error) {} }); @@ -103,7 +103,7 @@ describe("Rate Limiter Service", () => { storeClient: redisRateLimitClient, keyPrefix, points, - duration: 60, + duration: 60 }); expect(limiter.keyPrefix).toBe(keyPrefix); @@ -357,7 +357,7 @@ describe("Rate Limiter Service", () => { storeClient: redisRateLimitClient, keyPrefix, points, - duration, + duration }); const consumePoints = 5; diff --git a/apps/api/src/services/rate-limiter.ts b/apps/api/src/services/rate-limiter.ts index 5eecfa70..8067f862 100644 --- a/apps/api/src/services/rate-limiter.ts +++ b/apps/api/src/services/rate-limiter.ts @@ -18,7 +18,7 @@ const RATE_LIMITS = { etier2c: 300, etier1a: 1000, etier2a: 300, - etierscale1: 150, + etierscale1: 150 }, scrape: { default: 20, @@ -35,7 +35,7 @@ const RATE_LIMITS = { etier2c: 2500, etier1a: 1000, etier2a: 2500, - etierscale1: 1500, + etierscale1: 1500 }, search: { default: 20, @@ -52,9 +52,9 @@ const RATE_LIMITS = { etier2c: 2500, etier1a: 1000, etier2a: 2500, - etierscale1: 1500, + etierscale1: 1500 }, - map:{ + map: { default: 20, free: 5, starter: 50, @@ -69,36 +69,36 @@ const RATE_LIMITS = { etier2c: 2500, etier1a: 1000, etier2a: 2500, - etierscale1: 1500, + etierscale1: 1500 }, preview: { free: 5, - default: 5, + default: 5 }, account: { free: 100, - default: 100, + default: 100 }, crawlStatus: { free: 300, - default: 500, + default: 500 }, testSuite: { free: 10000, - default: 10000, - }, + default: 10000 + } }; export const redisRateLimitClient = new Redis( process.env.REDIS_RATE_LIMIT_URL! -) +); const createRateLimiter = (keyPrefix, points) => new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix, points, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); export const serverRateLimiter = createRateLimiter( @@ -110,43 +110,42 @@ export const testSuiteRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "test-suite", points: 10000, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); export const devBRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "dev-b", points: 1200, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); export const manualRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "manual", points: 2000, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); - export const scrapeStatusRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "scrape-status", points: 400, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); export const etier1aRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "etier1a", points: 10000, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); export const etier2aRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "etier2a", points: 2500, - duration: 60, // Duration in seconds + duration: 60 // Duration in seconds }); const testSuiteTokens = [ @@ -180,12 +179,12 @@ export function getRateLimiterPoints( token?: string, plan?: string, teamId?: string -) : number { +): number { const rateLimitConfig = RATE_LIMITS[mode]; // {default : 5} if (!rateLimitConfig) return RATE_LIMITS.account.default; - - const points : number = + + const points: number = rateLimitConfig[makePlanKey(plan)] || rateLimitConfig.default; // 5 return points; } @@ -195,30 +194,33 @@ export function getRateLimiter( token?: string, plan?: string, teamId?: string - ) : RateLimiterRedis { - if (token && testSuiteTokens.some(testToken => token.includes(testToken))) { +): RateLimiterRedis { + if (token && testSuiteTokens.some((testToken) => token.includes(testToken))) { return testSuiteRateLimiter; } - if(teamId && teamId === process.env.DEV_B_TEAM_ID) { + if (teamId && teamId === process.env.DEV_B_TEAM_ID) { return devBRateLimiter; } - - if(teamId && teamId === process.env.ETIER1A_TEAM_ID) { + + if (teamId && teamId === process.env.ETIER1A_TEAM_ID) { return etier1aRateLimiter; } - if(teamId && teamId === process.env.ETIER2A_TEAM_ID) { + if (teamId && teamId === process.env.ETIER2A_TEAM_ID) { return etier2aRateLimiter; } - if(teamId && teamId === process.env.ETIER2D_TEAM_ID) { + if (teamId && teamId === process.env.ETIER2D_TEAM_ID) { return etier2aRateLimiter; } - if(teamId && manual.includes(teamId)) { + if (teamId && manual.includes(teamId)) { return manualRateLimiter; } - - return createRateLimiter(`${mode}-${makePlanKey(plan)}`, getRateLimiterPoints(mode, token, plan, teamId)); + + return createRateLimiter( + `${mode}-${makePlanKey(plan)}`, + getRateLimiterPoints(mode, token, plan, teamId) + ); } diff --git a/apps/api/src/services/redis.ts b/apps/api/src/services/redis.ts index 1bd83605..04fcbd5e 100644 --- a/apps/api/src/services/redis.ts +++ b/apps/api/src/services/redis.ts @@ -35,7 +35,12 @@ redisRateLimitClient.on("connect", (err) => { * @param {string} value The value to store. * @param {number} [expire] Optional expiration time in seconds. */ -const setValue = async (key: string, value: string, expire?: number, nx = false) => { +const setValue = async ( + key: string, + value: string, + expire?: number, + nx = false +) => { if (expire && !nx) { await redisRateLimitClient.set(key, value, "EX", expire); } else { diff --git a/apps/api/src/services/redlock.ts b/apps/api/src/services/redlock.ts index 921a973a..757346f9 100644 --- a/apps/api/src/services/redlock.ts +++ b/apps/api/src/services/redlock.ts @@ -21,6 +21,6 @@ export const redlock = new Redlock( // The minimum remaining time on a lock before an extension is automatically // attempted with the `using` API. - automaticExtensionThreshold: 500, // time in ms + automaticExtensionThreshold: 500 // time in ms } ); diff --git a/apps/api/src/services/sentry.ts b/apps/api/src/services/sentry.ts index 072a501e..41f19362 100644 --- a/apps/api/src/services/sentry.ts +++ b/apps/api/src/services/sentry.ts @@ -7,12 +7,10 @@ if (process.env.SENTRY_DSN) { logger.info("Setting up Sentry..."); Sentry.init({ dsn: process.env.SENTRY_DSN, - integrations: [ - nodeProfilingIntegration(), - ], + integrations: [nodeProfilingIntegration()], tracesSampleRate: process.env.SENTRY_ENVIRONMENT === "dev" ? 1.0 : 0.045, profilesSampleRate: 1.0, serverName: process.env.FLY_MACHINE_ID, - environment: process.env.SENTRY_ENVIRONMENT ?? "production", + environment: process.env.SENTRY_ENVIRONMENT ?? "production" }); } diff --git a/apps/api/src/services/supabase.ts b/apps/api/src/services/supabase.ts index 61f16836..521a82ca 100644 --- a/apps/api/src/services/supabase.ts +++ b/apps/api/src/services/supabase.ts @@ -10,7 +10,7 @@ class SupabaseService { constructor() { const supabaseUrl = process.env.SUPABASE_URL; const supabaseServiceToken = process.env.SUPABASE_SERVICE_TOKEN; - const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true'; + const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; // Only initialize the Supabase client if both URL and Service Token are provided. if (!useDbAuthentication) { // Warn the user that Authentication is disabled by setting the client to null @@ -52,6 +52,6 @@ export const supabase_service: SupabaseClient = new Proxy( } // Otherwise, delegate access to the Supabase client. return Reflect.get(client, prop, receiver); - }, + } } ) as unknown as SupabaseClient; diff --git a/apps/api/src/services/system-monitor.ts b/apps/api/src/services/system-monitor.ts index b5e1bf29..4fa4c478 100644 --- a/apps/api/src/services/system-monitor.ts +++ b/apps/api/src/services/system-monitor.ts @@ -1,223 +1,228 @@ -import si from 'systeminformation'; +import si from "systeminformation"; import { Mutex } from "async-mutex"; -import os from 'os'; -import fs from 'fs'; -import { logger } from '../lib/logger'; +import os from "os"; +import fs from "fs"; +import { logger } from "../lib/logger"; const IS_KUBERNETES = process.env.IS_KUBERNETES === "true"; const MAX_CPU = process.env.MAX_CPU ? parseFloat(process.env.MAX_CPU) : 0.8; const MAX_RAM = process.env.MAX_RAM ? parseFloat(process.env.MAX_RAM) : 0.8; -const CACHE_DURATION = process.env.SYS_INFO_MAX_CACHE_DURATION ? parseFloat(process.env.SYS_INFO_MAX_CACHE_DURATION) : 150; - +const CACHE_DURATION = process.env.SYS_INFO_MAX_CACHE_DURATION + ? parseFloat(process.env.SYS_INFO_MAX_CACHE_DURATION) + : 150; class SystemMonitor { - private static instance: SystemMonitor; - private static instanceMutex = new Mutex(); + private static instance: SystemMonitor; + private static instanceMutex = new Mutex(); - private cpuUsageCache: number | null = null; - private memoryUsageCache: number | null = null; - private lastCpuCheck: number = 0; - private lastMemoryCheck: number = 0; - - // Variables for CPU usage calculation - private previousCpuUsage: number = 0; - private previousTime: number = Date.now(); + private cpuUsageCache: number | null = null; + private memoryUsageCache: number | null = null; + private lastCpuCheck: number = 0; + private lastMemoryCheck: number = 0; - private constructor() {} + // Variables for CPU usage calculation + private previousCpuUsage: number = 0; + private previousTime: number = Date.now(); - public static async getInstance(): Promise { - if (SystemMonitor.instance) { - return SystemMonitor.instance; - } - - await this.instanceMutex.runExclusive(async () => { - if (!SystemMonitor.instance) { - SystemMonitor.instance = new SystemMonitor(); - } - }); - - return SystemMonitor.instance; + private constructor() {} + + public static async getInstance(): Promise { + if (SystemMonitor.instance) { + return SystemMonitor.instance; } - public async checkMemoryUsage() { - if (IS_KUBERNETES) { - return this._checkMemoryUsageKubernetes(); - } - return this._checkMemoryUsage(); + await this.instanceMutex.runExclusive(async () => { + if (!SystemMonitor.instance) { + SystemMonitor.instance = new SystemMonitor(); + } + }); + + return SystemMonitor.instance; + } + + public async checkMemoryUsage() { + if (IS_KUBERNETES) { + return this._checkMemoryUsageKubernetes(); + } + return this._checkMemoryUsage(); + } + + private readMemoryCurrent(): number { + const data = fs.readFileSync("/sys/fs/cgroup/memory.current", "utf8"); + return parseInt(data.trim(), 10); + } + + private readMemoryMax(): number { + const data = fs.readFileSync("/sys/fs/cgroup/memory.max", "utf8").trim(); + if (data === "max") { + return Infinity; + } + return parseInt(data, 10); + } + private async _checkMemoryUsageKubernetes() { + try { + const currentMemoryUsage = this.readMemoryCurrent(); + const memoryLimit = this.readMemoryMax(); + + let memoryUsagePercentage: number; + + if (memoryLimit === Infinity) { + // No memory limit set; use total system memory + const totalMemory = os.totalmem(); + memoryUsagePercentage = currentMemoryUsage / totalMemory; + } else { + memoryUsagePercentage = currentMemoryUsage / memoryLimit; + } + + // console.log("Memory usage:", memoryUsagePercentage); + + return memoryUsagePercentage; + } catch (error) { + logger.error(`Error calculating memory usage: ${error}`); + return 0; // Fallback to 0% usage + } + } + + private async _checkMemoryUsage() { + const now = Date.now(); + if ( + this.memoryUsageCache !== null && + now - this.lastMemoryCheck < CACHE_DURATION + ) { + return this.memoryUsageCache; } + const memoryData = await si.mem(); + const totalMemory = memoryData.total; + const availableMemory = memoryData.available; + const usedMemory = totalMemory - availableMemory; + const usedMemoryPercentage = usedMemory / totalMemory; - private readMemoryCurrent(): number { - const data = fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf8'); - return parseInt(data.trim(), 10); + this.memoryUsageCache = usedMemoryPercentage; + this.lastMemoryCheck = now; + + return usedMemoryPercentage; + } + + public async checkCpuUsage() { + if (IS_KUBERNETES) { + return this._checkCpuUsageKubernetes(); + } + return this._checkCpuUsage(); + } + private readCpuUsage(): number { + const data = fs.readFileSync("/sys/fs/cgroup/cpu.stat", "utf8"); + const match = data.match(/^usage_usec (\d+)$/m); + if (match) { + return parseInt(match[1], 10); + } + throw new Error("Could not read usage_usec from cpu.stat"); + } + + private getNumberOfCPUs(): number { + let cpus: number[] = []; + try { + const cpusetPath = "/sys/fs/cgroup/cpuset.cpus.effective"; + const data = fs.readFileSync(cpusetPath, "utf8").trim(); + + if (!data) { + throw new Error(`${cpusetPath} is empty.`); + } + + cpus = this.parseCpuList(data); + + if (cpus.length === 0) { + throw new Error("No CPUs found in cpuset.cpus.effective"); + } + } catch (error) { + logger.warn( + `Unable to read cpuset.cpus.effective, defaulting to OS CPUs: ${error}` + ); + cpus = os.cpus().map((cpu, index) => index); + } + return cpus.length; + } + + private parseCpuList(cpuList: string): number[] { + const ranges = cpuList.split(","); + const cpus: number[] = []; + ranges.forEach((range) => { + const [startStr, endStr] = range.split("-"); + const start = parseInt(startStr, 10); + const end = endStr !== undefined ? parseInt(endStr, 10) : start; + for (let i = start; i <= end; i++) { + cpus.push(i); + } + }); + return cpus; + } + private async _checkCpuUsageKubernetes() { + try { + const usage = this.readCpuUsage(); // In microseconds (µs) + const now = Date.now(); + + // Check if it's the first run + if (this.previousCpuUsage === 0) { + // Initialize previous values + this.previousCpuUsage = usage; + this.previousTime = now; + // Return 0% CPU usage on first run + return 0; + } + + const deltaUsage = usage - this.previousCpuUsage; // In µs + const deltaTime = (now - this.previousTime) * 1000; // Convert ms to µs + + const numCPUs = this.getNumberOfCPUs(); // Get the number of CPUs + + // Calculate the CPU usage percentage and normalize by the number of CPUs + const cpuUsagePercentage = deltaUsage / deltaTime / numCPUs; + + // Update previous values + this.previousCpuUsage = usage; + this.previousTime = now; + + // console.log("CPU usage:", cpuUsagePercentage); + + return cpuUsagePercentage; + } catch (error) { + logger.error(`Error calculating CPU usage: ${error}`); + return 0; // Fallback to 0% usage + } + } + + private async _checkCpuUsage() { + const now = Date.now(); + if ( + this.cpuUsageCache !== null && + now - this.lastCpuCheck < CACHE_DURATION + ) { + return this.cpuUsageCache; } - private readMemoryMax(): number { - const data = fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim(); - if (data === 'max') { - return Infinity; - } - return parseInt(data, 10); - } - private async _checkMemoryUsageKubernetes() { - try { - const currentMemoryUsage = this.readMemoryCurrent(); - const memoryLimit = this.readMemoryMax(); + const cpuData = await si.currentLoad(); + const cpuLoad = cpuData.currentLoad / 100; - let memoryUsagePercentage: number; + this.cpuUsageCache = cpuLoad; + this.lastCpuCheck = now; - if (memoryLimit === Infinity) { - // No memory limit set; use total system memory - const totalMemory = os.totalmem(); - memoryUsagePercentage = currentMemoryUsage / totalMemory; - } else { - memoryUsagePercentage = currentMemoryUsage / memoryLimit; - } + return cpuLoad; + } - // console.log("Memory usage:", memoryUsagePercentage); + public async acceptConnection() { + const cpuUsage = await this.checkCpuUsage(); + const memoryUsage = await this.checkMemoryUsage(); - return memoryUsagePercentage; - } catch (error) { - logger.error(`Error calculating memory usage: ${error}`); - return 0; // Fallback to 0% usage - } - } + return cpuUsage < MAX_CPU && memoryUsage < MAX_RAM; + } - private async _checkMemoryUsage() { - const now = Date.now(); - if (this.memoryUsageCache !== null && (now - this.lastMemoryCheck) < CACHE_DURATION) { - return this.memoryUsageCache; - } - - const memoryData = await si.mem(); - const totalMemory = memoryData.total; - const availableMemory = memoryData.available; - const usedMemory = totalMemory - availableMemory; - const usedMemoryPercentage = (usedMemory / totalMemory); - - this.memoryUsageCache = usedMemoryPercentage; - this.lastMemoryCheck = now; - - return usedMemoryPercentage; - } - - public async checkCpuUsage() { - if (IS_KUBERNETES) { - return this._checkCpuUsageKubernetes(); - } - return this._checkCpuUsage(); - } - private readCpuUsage(): number { - const data = fs.readFileSync('/sys/fs/cgroup/cpu.stat', 'utf8'); - const match = data.match(/^usage_usec (\d+)$/m); - if (match) { - return parseInt(match[1], 10); - } - throw new Error('Could not read usage_usec from cpu.stat'); - } - - - private getNumberOfCPUs(): number { - let cpus: number[] = []; - try { - const cpusetPath = '/sys/fs/cgroup/cpuset.cpus.effective'; - const data = fs.readFileSync(cpusetPath, 'utf8').trim(); - - if (!data) { - throw new Error(`${cpusetPath} is empty.`); - } - - cpus = this.parseCpuList(data); - - if (cpus.length === 0) { - throw new Error('No CPUs found in cpuset.cpus.effective'); - } - } catch (error) { - logger.warn(`Unable to read cpuset.cpus.effective, defaulting to OS CPUs: ${error}`); - cpus = os.cpus().map((cpu, index) => index); - } - return cpus.length; - } - - - private parseCpuList(cpuList: string): number[] { - const ranges = cpuList.split(','); - const cpus: number[] = []; - ranges.forEach((range) => { - const [startStr, endStr] = range.split('-'); - const start = parseInt(startStr, 10); - const end = endStr !== undefined ? parseInt(endStr, 10) : start; - for (let i = start; i <= end; i++) { - cpus.push(i); - } - }); - return cpus; - } - private async _checkCpuUsageKubernetes() { - try { - const usage = this.readCpuUsage(); // In microseconds (µs) - const now = Date.now(); - - // Check if it's the first run - if (this.previousCpuUsage === 0) { - // Initialize previous values - this.previousCpuUsage = usage; - this.previousTime = now; - // Return 0% CPU usage on first run - return 0; - } - - const deltaUsage = usage - this.previousCpuUsage; // In µs - const deltaTime = (now - this.previousTime) * 1000; // Convert ms to µs - - const numCPUs = this.getNumberOfCPUs(); // Get the number of CPUs - - // Calculate the CPU usage percentage and normalize by the number of CPUs - const cpuUsagePercentage = (deltaUsage / deltaTime) / numCPUs; - - // Update previous values - this.previousCpuUsage = usage; - this.previousTime = now; - - // console.log("CPU usage:", cpuUsagePercentage); - - return cpuUsagePercentage; - } catch (error) { - logger.error(`Error calculating CPU usage: ${error}`); - return 0; // Fallback to 0% usage - } - } - - - private async _checkCpuUsage() { - const now = Date.now(); - if (this.cpuUsageCache !== null && (now - this.lastCpuCheck) < CACHE_DURATION) { - return this.cpuUsageCache; - } - - const cpuData = await si.currentLoad(); - const cpuLoad = cpuData.currentLoad / 100; - - this.cpuUsageCache = cpuLoad; - this.lastCpuCheck = now; - - return cpuLoad; - } - - public async acceptConnection() { - const cpuUsage = await this.checkCpuUsage(); - const memoryUsage = await this.checkMemoryUsage(); - - return cpuUsage < MAX_CPU && memoryUsage < MAX_RAM; - } - - public clearCache() { - this.cpuUsageCache = null; - this.memoryUsageCache = null; - this.lastCpuCheck = 0; - this.lastMemoryCheck = 0; - } + public clearCache() { + this.cpuUsageCache = null; + this.memoryUsageCache = null; + this.lastCpuCheck = 0; + this.lastMemoryCheck = 0; + } } -export default SystemMonitor.getInstance(); \ No newline at end of file +export default SystemMonitor.getInstance(); diff --git a/apps/api/src/services/webhook.ts b/apps/api/src/services/webhook.ts index 7840484d..dfee11f6 100644 --- a/apps/api/src/services/webhook.ts +++ b/apps/api/src/services/webhook.ts @@ -22,7 +22,9 @@ export const callWebhook = async ( id ); const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; - let webhookUrl = specified ?? (selfHostedUrl ? webhookSchema.parse({ url: selfHostedUrl }) : undefined); + let webhookUrl = + specified ?? + (selfHostedUrl ? webhookSchema.parse({ url: selfHostedUrl }) : undefined); // Only fetch the webhook URL from the database if the self-hosted webhook URL and specified webhook are not set // and the USE_DB_AUTHENTICATION environment variable is set to true @@ -46,7 +48,14 @@ export const callWebhook = async ( webhookUrl = webhooksData[0].url; } - logger.debug("Calling webhook...", { webhookUrl, teamId, specified, v1, eventType, awaitWebhook }); + logger.debug("Calling webhook...", { + webhookUrl, + teamId, + specified, + v1, + eventType, + awaitWebhook + }); if (!webhookUrl) { return null; @@ -61,14 +70,12 @@ export const callWebhook = async ( ) { for (let i = 0; i < data.result.links.length; i++) { if (v1) { - dataToSend.push( - data.result.links[i].content - ); + dataToSend.push(data.result.links[i].content); } else { dataToSend.push({ content: data.result.links[i].content.content, markdown: data.result.links[i].content.markdown, - metadata: data.result.links[i].content.metadata, + metadata: data.result.links[i].content.metadata }); } } @@ -82,23 +89,23 @@ export const callWebhook = async ( success: !v1 ? data.success : eventType === "crawl.page" - ? data.success - : true, + ? data.success + : true, type: eventType, [v1 ? "id" : "jobId"]: id, data: dataToSend, error: !v1 ? data?.error || undefined : eventType === "crawl.page" - ? data?.error || undefined - : undefined, + ? data?.error || undefined + : undefined }, { headers: { "Content-Type": "application/json", - ...webhookUrl.headers, + ...webhookUrl.headers }, - timeout: v1 ? 10000 : 30000, // 10 seconds timeout (v1) + timeout: v1 ? 10000 : 30000 // 10 seconds timeout (v1) } ); } catch (error) { @@ -114,22 +121,22 @@ export const callWebhook = async ( success: !v1 ? data.success : eventType === "crawl.page" - ? data.success - : true, + ? data.success + : true, type: eventType, [v1 ? "id" : "jobId"]: id, data: dataToSend, error: !v1 ? data?.error || undefined : eventType === "crawl.page" - ? data?.error || undefined - : undefined, + ? data?.error || undefined + : undefined }, { headers: { "Content-Type": "application/json", - ...webhookUrl.headers, - }, + ...webhookUrl.headers + } } ) .catch((error) => { diff --git a/apps/api/src/strings.ts b/apps/api/src/strings.ts index 8edc57f1..d5672b82 100644 --- a/apps/api/src/strings.ts +++ b/apps/api/src/strings.ts @@ -1,4 +1,4 @@ export const errorNoResults = "No results found, please check the URL or contact us at help@mendable.ai to file a ticket."; -export const clientSideError = "client-side exception has occurred" \ No newline at end of file +export const clientSideError = "client-side exception has occurred"; diff --git a/apps/api/src/supabase_types.ts b/apps/api/src/supabase_types.ts index 00b2efbb..8f9e1b64 100644 --- a/apps/api/src/supabase_types.ts +++ b/apps/api/src/supabase_types.ts @@ -40,7 +40,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; company: { @@ -77,7 +77,7 @@ export interface Database { columns: ["pricing_plan_id"]; referencedRelation: "pricing_plan"; referencedColumns: ["id"]; - }, + } ]; }; constants: { @@ -126,7 +126,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; customers: { @@ -157,7 +157,7 @@ export interface Database { columns: ["user_id"]; referencedRelation: "users"; referencedColumns: ["id"]; - }, + } ]; }; data: { @@ -236,7 +236,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; data_partitioned: { @@ -390,7 +390,7 @@ export interface Database { columns: ["company_id"]; referencedRelation: "company"; referencedColumns: ["company_id"]; - }, + } ]; }; message: { @@ -439,7 +439,7 @@ export interface Database { columns: ["conversation_id"]; referencedRelation: "conversation"; referencedColumns: ["conversation_id"]; - }, + } ]; }; model_configuration: { @@ -479,7 +479,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; monthly_message_counts: { @@ -507,7 +507,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; prices: { @@ -560,7 +560,7 @@ export interface Database { columns: ["product_id"]; referencedRelation: "products"; referencedColumns: ["id"]; - }, + } ]; }; pricing_plan: { @@ -747,7 +747,7 @@ export interface Database { columns: ["user_id"]; referencedRelation: "users"; referencedColumns: ["id"]; - }, + } ]; }; suggested_questions: { @@ -775,7 +775,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; user_notifications: { @@ -821,7 +821,7 @@ export interface Database { columns: ["user_id"]; referencedRelation: "users"; referencedColumns: ["id"]; - }, + } ]; }; users: { @@ -864,7 +864,7 @@ export interface Database { columns: ["id"]; referencedRelation: "users"; referencedColumns: ["id"]; - }, + } ]; }; z_testcomp_92511: { @@ -934,7 +934,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - }, + } ]; }; }; diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index bf7d2248..cfae8f23 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -1,5 +1,10 @@ import { z } from "zod"; -import { AuthCreditUsageChunk, ScrapeOptions, Document as V1Document, webhookSchema } from "./controllers/v1/types"; +import { + AuthCreditUsageChunk, + ScrapeOptions, + Document as V1Document, + webhookSchema +} from "./controllers/v1/types"; import { ExtractorOptions, Document } from "./lib/entities"; import { InternalOptions } from "./scraper/scrapeURL"; @@ -52,13 +57,15 @@ export interface RunWebScraperParams { is_scrape?: boolean; } -export type RunWebScraperResult = { - success: false; - error: Error; -} | { - success: true; - document: V1Document; -} +export type RunWebScraperResult = + | { + success: false; + error: Error; + } + | { + success: true; + document: V1Document; + }; export interface FirecrawlJob { job_id?: string; @@ -73,8 +80,8 @@ export interface FirecrawlJob { crawlerOptions?: any; scrapeOptions?: any; origin: string; - num_tokens?: number, - retry?: boolean, + num_tokens?: number; + retry?: boolean; crawl_id?: string; } @@ -92,7 +99,6 @@ export interface FirecrawlCrawlResponse { body: { status: string; jobId: string; - }; error?: string; } @@ -101,7 +107,7 @@ export interface FirecrawlCrawlStatusResponse { statusCode: number; body: { status: string; - data: Document[]; + data: Document[]; }; error?: string; } @@ -121,29 +127,29 @@ export enum RateLimiterMode { Scrape = "scrape", Preview = "preview", Search = "search", - Map = "map", - + Map = "map" } -export type AuthResponse = { - success: true; - team_id: string; - api_key?: string; - plan?: PlanType; - chunk: AuthCreditUsageChunk | null; -} | { - success: false; - error: string; - status: number; -} - +export type AuthResponse = + | { + success: true; + team_id: string; + api_key?: string; + plan?: PlanType; + chunk: AuthCreditUsageChunk | null; + } + | { + success: false; + error: string; + status: number; + }; export enum NotificationType { APPROACHING_LIMIT = "approachingLimit", LIMIT_REACHED = "limitReached", RATE_LIMIT_REACHED = "rateLimitReached", AUTO_RECHARGE_SUCCESS = "autoRechargeSuccess", - AUTO_RECHARGE_FAILED = "autoRechargeFailed", + AUTO_RECHARGE_FAILED = "autoRechargeFailed" } export type ScrapeLog = { @@ -161,7 +167,7 @@ export type ScrapeLog = { ipv6_support?: boolean | null; }; -export type PlanType = +export type PlanType = | "starter" | "standard" | "scale" @@ -175,5 +181,11 @@ export type PlanType = | "free" | ""; - -export type WebhookEventType = "crawl.page" | "batch_scrape.page" | "crawl.started" | "batch_scrape.started" | "crawl.completed" | "batch_scrape.completed" | "crawl.failed"; \ No newline at end of file +export type WebhookEventType = + | "crawl.page" + | "batch_scrape.page" + | "crawl.started" + | "batch_scrape.started" + | "crawl.completed" + | "batch_scrape.completed" + | "crawl.failed"; From 52f2e733e2a8a25827a9e5d9c64e7a6b652f49ea Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 11 Dec 2024 19:48:22 -0300 Subject: [PATCH 22/52] Nick: fixes --- apps/api/package.json | 3 ++- apps/api/pnpm-lock.yaml | 18 ++++++++++++++---- apps/api/src/controllers/v0/crawl.ts | 18 +++++++----------- apps/api/src/controllers/v0/scrape.ts | 10 ++++------ apps/api/src/index.ts | 14 ++++++-------- apps/api/src/routes/v1.ts | 24 ++++++++++-------------- 6 files changed, 43 insertions(+), 44 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 86f798e9..1f4fd8a8 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "nodemon --exec ts-node src/index.ts", "start:production": "tsc && node dist/src/index.js", - "format": "npx prettier --write \"src/**/*.(js|ts)\"", + "format": "prettier --write \"src/**/*.(js|ts)\"", "flyio": "node dist/src/index.js", "start:dev": "nodemon --exec ts-node src/index.ts", "build": "tsc && pnpm sentry:sourcemaps", @@ -102,6 +102,7 @@ "pdf-parse": "^1.1.1", "pos": "^0.4.2", "posthog-node": "^4.0.1", + "prettier": "^3.4.2", "promptable": "^0.0.10", "puppeteer": "^22.12.1", "rate-limiter-flexible": "2.4.2", diff --git a/apps/api/pnpm-lock.yaml b/apps/api/pnpm-lock.yaml index 4557afa9..563965c1 100644 --- a/apps/api/pnpm-lock.yaml +++ b/apps/api/pnpm-lock.yaml @@ -164,6 +164,9 @@ importers: posthog-node: specifier: ^4.0.1 version: 4.0.1 + prettier: + specifier: ^3.4.2 + version: 3.4.2 promptable: specifier: ^0.0.10 version: 0.0.10 @@ -3756,6 +3759,11 @@ packages: resolution: {integrity: sha512-rtqm2h22QxLGBrW2bLYzbRhliIrqgZ0k+gF0LkQ1SNdeD06YE5eilV0MxZppFSxC8TfH0+B0cWCuebEnreIDgQ==} engines: {node: '>=15.0.0'} + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4321,8 +4329,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true @@ -8978,6 +8986,8 @@ snapshots: transitivePeerDependencies: - debug + prettier@3.4.2: {} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -9000,7 +9010,7 @@ snapshots: csv-parse: 5.5.6 gpt3-tokenizer: 1.1.5 openai: 3.3.0 - typescript: 5.6.3 + typescript: 5.7.2 uuid: 9.0.1 zod: 3.23.8 transitivePeerDependencies: @@ -9584,7 +9594,7 @@ snapshots: typescript@5.4.5: {} - typescript@5.6.3: {} + typescript@5.7.2: {} typesense@1.8.2(@babel/runtime@7.24.6): dependencies: diff --git a/apps/api/src/controllers/v0/crawl.ts b/apps/api/src/controllers/v0/crawl.ts index b8c6bc63..bb9ba363 100644 --- a/apps/api/src/controllers/v0/crawl.ts +++ b/apps/api/src/controllers/v0/crawl.ts @@ -86,12 +86,10 @@ export async function crawlController(req: Request, res: Response) { } = await checkTeamCredits(chunk, team_id, limitCheck); if (!creditsCheckSuccess) { - return res - .status(402) - .json({ - error: - "Insufficient credits. You may be requesting with a higher limit than the amount of credits you have left. If not, upgrade your plan at https://firecrawl.dev/pricing or contact us at help@firecrawl.com" - }); + return res.status(402).json({ + error: + "Insufficient credits. You may be requesting with a higher limit than the amount of credits you have left. If not, upgrade your plan at https://firecrawl.dev/pricing or contact us at help@firecrawl.com" + }); } // TODO: need to do this to v1 @@ -259,10 +257,8 @@ export async function crawlController(req: Request, res: Response) { } catch (error) { Sentry.captureException(error); logger.error(error); - return res - .status(500) - .json({ - error: error instanceof ZodError ? "Invalid URL" : error.message - }); + return res.status(500).json({ + error: error instanceof ZodError ? "Invalid URL" : error.message + }); } } diff --git a/apps/api/src/controllers/v0/scrape.ts b/apps/api/src/controllers/v0/scrape.ts index c7c8d9fe..4a761ea3 100644 --- a/apps/api/src/controllers/v0/scrape.ts +++ b/apps/api/src/controllers/v0/scrape.ts @@ -211,12 +211,10 @@ export async function scrapeController(req: Request, res: Response) { await checkTeamCredits(chunk, team_id, 1); if (!creditsCheckSuccess) { earlyReturn = true; - return res - .status(402) - .json({ - error: - "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing" - }); + return res.status(402).json({ + error: + "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing" + }); } } catch (error) { logger.error(error); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 905c32d8..a4f4445b 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -235,14 +235,12 @@ app.use( " -- " + verbose ); - res - .status(500) - .json({ - success: false, - error: - "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + - id - }); + res.status(500).json({ + success: false, + error: + "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + + id + }); } ); diff --git a/apps/api/src/routes/v1.ts b/apps/api/src/routes/v1.ts index 206423ba..a9727e00 100644 --- a/apps/api/src/routes/v1.ts +++ b/apps/api/src/routes/v1.ts @@ -54,13 +54,11 @@ function checkCreditsMiddleware( `Insufficient credits: ${JSON.stringify({ team_id: req.auth.team_id, minimum, remainingCredits })}` ); if (!res.headersSent) { - return res - .status(402) - .json({ - success: false, - error: - "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing or try changing the request limit to a lower value." - }); + return res.status(402).json({ + success: false, + error: + "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing or try changing the request limit to a lower value." + }); } } req.account = { remainingCredits }; @@ -122,13 +120,11 @@ function idempotencyMiddleware( function blocklistMiddleware(req: Request, res: Response, next: NextFunction) { if (typeof req.body.url === "string" && isUrlBlocked(req.body.url)) { if (!res.headersSent) { - return res - .status(403) - .json({ - success: false, - error: - "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions." - }); + return res.status(403).json({ + success: false, + error: + "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions." + }); } } next(); From 8a1c4049188b0899121c5e1bab528c7a2d3b49c1 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 11 Dec 2024 19:51:08 -0300 Subject: [PATCH 23/52] Nick: revert trailing comma --- apps/api/.prettierrc | 2 +- .../src/__tests__/e2e_extract/index.test.ts | 76 +-- .../__tests__/e2e_full_withAuth/index.test.ts | 268 +++++----- apps/api/src/__tests__/e2e_map/index.test.ts | 24 +- .../src/__tests__/e2e_noAuth/index.test.ts | 16 +- .../__tests__/e2e_v1_withAuth/index.test.ts | 158 +++--- .../e2e_v1_withAuth_all_params/index.test.ts | 214 ++++---- .../src/__tests__/e2e_withAuth/index.test.ts | 154 +++--- .../src/controllers/__tests__/crawl.test.ts | 14 +- apps/api/src/controllers/auth.ts | 40 +- apps/api/src/controllers/v0/admin/queue.ts | 44 +- .../src/controllers/v0/admin/redis-health.ts | 6 +- apps/api/src/controllers/v0/crawl-cancel.ts | 2 +- apps/api/src/controllers/v0/crawl-status.ts | 10 +- apps/api/src/controllers/v0/crawl.ts | 34 +- apps/api/src/controllers/v0/crawlPreview.ts | 18 +- apps/api/src/controllers/v0/scrape.ts | 50 +- apps/api/src/controllers/v0/search.ts | 32 +- apps/api/src/controllers/v0/status.ts | 8 +- .../v1/__tests__/urlValidation.test.ts | 14 +- apps/api/src/controllers/v1/batch-scrape.ts | 30 +- .../src/controllers/v1/concurrency-check.ts | 6 +- apps/api/src/controllers/v1/crawl-cancel.ts | 4 +- .../api/src/controllers/v1/crawl-status-ws.ts | 31 +- apps/api/src/controllers/v1/crawl-status.ts | 24 +- apps/api/src/controllers/v1/crawl.ts | 46 +- apps/api/src/controllers/v1/extract.ts | 53 +- apps/api/src/controllers/v1/map.ts | 32 +- apps/api/src/controllers/v1/scrape-status.ts | 10 +- apps/api/src/controllers/v1/scrape.ts | 24 +- apps/api/src/controllers/v1/types.ts | 118 ++-- apps/api/src/index.ts | 30 +- apps/api/src/lib/LLM-extraction/index.ts | 10 +- apps/api/src/lib/LLM-extraction/models.ts | 30 +- .../lib/__tests__/html-to-markdown.test.ts | 8 +- .../src/lib/__tests__/job-priority.test.ts | 18 +- apps/api/src/lib/batch-process.ts | 2 +- apps/api/src/lib/cache.ts | 6 +- apps/api/src/lib/concurrency-limit.ts | 18 +- apps/api/src/lib/crawl-redis.test.ts | 8 +- apps/api/src/lib/crawl-redis.ts | 52 +- apps/api/src/lib/custom-error.ts | 2 +- apps/api/src/lib/default-values.ts | 8 +- apps/api/src/lib/extract/reranker.ts | 8 +- apps/api/src/lib/html-to-markdown.ts | 12 +- apps/api/src/lib/job-priority.ts | 6 +- apps/api/src/lib/logger.ts | 18 +- apps/api/src/lib/map-cosine.ts | 4 +- apps/api/src/lib/ranker.test.ts | 6 +- apps/api/src/lib/ranker.ts | 12 +- apps/api/src/lib/scrape-events.ts | 10 +- apps/api/src/lib/validate-country.ts | 502 +++++++++--------- apps/api/src/lib/validateUrl.test.ts | 24 +- apps/api/src/lib/withAuth.ts | 2 +- apps/api/src/main/runWebScraper.ts | 30 +- apps/api/src/routes/admin.ts | 12 +- apps/api/src/routes/v1.ts | 36 +- apps/api/src/run-req.ts | 22 +- .../WebScraper/__tests__/crawler.test.ts | 10 +- apps/api/src/scraper/WebScraper/crawler.ts | 50 +- .../WebScraper/custom/handleCustomScraping.ts | 18 +- apps/api/src/scraper/WebScraper/sitemap.ts | 20 +- .../utils/__tests__/blocklist.test.ts | 16 +- .../src/scraper/WebScraper/utils/blocklist.ts | 8 +- .../scraper/WebScraper/utils/maxDepthUtils.ts | 2 +- .../scraper/scrapeURL/engines/cache/index.ts | 2 +- .../scraper/scrapeURL/engines/docx/index.ts | 2 +- .../scraper/scrapeURL/engines/fetch/index.ts | 12 +- .../engines/fire-engine/checkStatus.ts | 36 +- .../scrapeURL/engines/fire-engine/delete.ts | 12 +- .../scrapeURL/engines/fire-engine/index.ts | 94 ++-- .../scrapeURL/engines/fire-engine/scrape.ts | 18 +- .../src/scraper/scrapeURL/engines/index.ts | 78 +-- .../scraper/scrapeURL/engines/pdf/index.ts | 52 +- .../scrapeURL/engines/playwright/index.ts | 16 +- .../scrapeURL/engines/scrapingbee/index.ts | 24 +- .../scrapeURL/engines/utils/downloadFile.ts | 14 +- .../engines/utils/specialtyHandler.ts | 8 +- apps/api/src/scraper/scrapeURL/error.ts | 7 +- apps/api/src/scraper/scrapeURL/index.ts | 58 +- .../src/scraper/scrapeURL/lib/extractLinks.ts | 2 +- .../scraper/scrapeURL/lib/extractMetadata.ts | 4 +- apps/api/src/scraper/scrapeURL/lib/fetch.ts | 56 +- .../scrapeURL/lib/removeUnwantedElements.ts | 10 +- .../scrapeURL/lib/urlSpecificParams.ts | 6 +- .../src/scraper/scrapeURL/scrapeURL.test.ts | 96 ++-- .../scraper/scrapeURL/transformers/cache.ts | 4 +- .../scraper/scrapeURL/transformers/index.ts | 46 +- .../scrapeURL/transformers/llmExtract.ts | 46 +- .../transformers/removeBase64Images.ts | 2 +- .../transformers/uploadScreenshot.ts | 4 +- apps/api/src/search/fireEngine.ts | 10 +- apps/api/src/search/googlesearch.ts | 18 +- apps/api/src/search/index.ts | 8 +- apps/api/src/search/searchapi.ts | 10 +- apps/api/src/search/serper.ts | 10 +- apps/api/src/services/alerts/index.ts | 10 +- apps/api/src/services/alerts/slack.ts | 8 +- apps/api/src/services/billing/auto_charge.ts | 34 +- .../src/services/billing/credit_billing.ts | 48 +- .../api/src/services/billing/issue_credits.ts | 2 +- apps/api/src/services/billing/stripe.ts | 10 +- apps/api/src/services/logging/crawl_log.ts | 4 +- apps/api/src/services/logging/log_job.ts | 20 +- apps/api/src/services/logging/scrape_log.ts | 6 +- .../notification/email_notification.ts | 34 +- .../notification/notification_string.ts | 2 +- apps/api/src/services/posthog.ts | 4 +- apps/api/src/services/queue-jobs.ts | 32 +- apps/api/src/services/queue-service.ts | 12 +- apps/api/src/services/queue-worker.ts | 132 ++--- apps/api/src/services/rate-limiter.test.ts | 82 +-- apps/api/src/services/rate-limiter.ts | 44 +- apps/api/src/services/redis.ts | 2 +- apps/api/src/services/redlock.ts | 4 +- apps/api/src/services/sentry.ts | 2 +- apps/api/src/services/supabase.ts | 8 +- apps/api/src/services/system-monitor.ts | 2 +- apps/api/src/services/webhook.ts | 32 +- apps/api/src/supabase_types.ts | 30 +- apps/api/src/types.ts | 6 +- 121 files changed, 1965 insertions(+), 1952 deletions(-) diff --git a/apps/api/.prettierrc b/apps/api/.prettierrc index d93a7f24..5d50a9cd 100644 --- a/apps/api/.prettierrc +++ b/apps/api/.prettierrc @@ -1,3 +1,3 @@ { - "trailingComma": "none" + "trailingComma": "all" } \ No newline at end of file diff --git a/apps/api/src/__tests__/e2e_extract/index.test.ts b/apps/api/src/__tests__/e2e_extract/index.test.ts index 117cbab1..e1e4d1ce 100644 --- a/apps/api/src/__tests__/e2e_extract/index.test.ts +++ b/apps/api/src/__tests__/e2e_extract/index.test.ts @@ -3,7 +3,7 @@ import dotenv from "dotenv"; import { FirecrawlCrawlResponse, FirecrawlCrawlStatusResponse, - FirecrawlScrapeResponse + FirecrawlScrapeResponse, } from "../../types"; dotenv.config(); @@ -23,9 +23,9 @@ describe("E2E Tests for Extract API Routes", () => { schema: { type: "object", properties: { - authors: { type: "array", items: { type: "string" } } - } - } + authors: { type: "array", items: { type: "string" } }, + }, + }, }); console.log(response.body); @@ -45,7 +45,7 @@ describe("E2E Tests for Extract API Routes", () => { expect(gotItRight).toBeGreaterThan(1); }, - 60000 + 60000, ); it.concurrent( @@ -62,9 +62,9 @@ describe("E2E Tests for Extract API Routes", () => { schema: { type: "object", properties: { - founders: { type: "array", items: { type: "string" } } - } - } + founders: { type: "array", items: { type: "string" } }, + }, + }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -83,7 +83,7 @@ describe("E2E Tests for Extract API Routes", () => { expect(gotItRight).toBeGreaterThanOrEqual(2); }, - 60000 + 60000, ); it.concurrent( @@ -100,10 +100,10 @@ describe("E2E Tests for Extract API Routes", () => { schema: { type: "array", items: { - type: "string" + type: "string", }, - required: ["items"] - } + required: ["items"], + }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -118,7 +118,7 @@ describe("E2E Tests for Extract API Routes", () => { expect(gotItRight).toBeGreaterThan(2); }, - 60000 + 60000, ); it.concurrent( @@ -135,15 +135,15 @@ describe("E2E Tests for Extract API Routes", () => { schema: { type: "object", properties: { - pciDssCompliance: { type: "boolean" } - } - } + pciDssCompliance: { type: "boolean" }, + }, + }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); expect(response.body.data?.pciDssCompliance).toBe(true); }, - 60000 + 60000, ); it.concurrent( @@ -163,10 +163,10 @@ describe("E2E Tests for Extract API Routes", () => { properties: { connector: { type: "string" }, description: { type: "string" }, - supportsCaptureDelete: { type: "boolean" } - } - } - } + supportsCaptureDelete: { type: "boolean" }, + }, + }, + }, }); console.log(response.body); @@ -174,7 +174,7 @@ describe("E2E Tests for Extract API Routes", () => { // expect(response.body).toHaveProperty("data"); // expect(response.body.data?.pciDssCompliance).toBe(true); }, - 60000 + 60000, ); it.concurrent( @@ -186,17 +186,17 @@ describe("E2E Tests for Extract API Routes", () => { .set("Content-Type", "application/json") .send({ urls: [ - "https://careers.abnormalsecurity.com/jobs/6119456003?gh_jid=6119456003" + "https://careers.abnormalsecurity.com/jobs/6119456003?gh_jid=6119456003", ], prompt: "what applicant tracking system is this company using?", schema: { type: "object", properties: { isGreenhouseATS: { type: "boolean" }, - answer: { type: "string" } - } + answer: { type: "string" }, + }, }, - allowExternalLinks: true + allowExternalLinks: true, }); console.log(response.body); @@ -204,7 +204,7 @@ describe("E2E Tests for Extract API Routes", () => { expect(response.body).toHaveProperty("data"); expect(response.body.data?.isGreenhouseATS).toBe(true); }, - 60000 + 60000, ); it.concurrent( @@ -222,12 +222,12 @@ describe("E2E Tests for Extract API Routes", () => { items: { type: "object", properties: { - component: { type: "string" } - } + component: { type: "string" }, + }, }, - required: ["items"] + required: ["items"], }, - allowExternalLinks: true + allowExternalLinks: true, }); console.log(response.body.data?.items); @@ -248,7 +248,7 @@ describe("E2E Tests for Extract API Routes", () => { } expect(gotItRight).toBeGreaterThan(2); }, - 60000 + 60000, ); it.concurrent( @@ -267,11 +267,11 @@ describe("E2E Tests for Extract API Routes", () => { properties: { name: { type: "string" }, work: { type: "string" }, - education: { type: "string" } + education: { type: "string" }, }, - required: ["name", "work", "education"] + required: ["name", "work", "education"], }, - allowExternalLinks: true + allowExternalLinks: true, }); console.log(response.body.data); @@ -281,7 +281,7 @@ describe("E2E Tests for Extract API Routes", () => { expect(response.body.data?.work).toBeDefined(); expect(response.body.data?.education).toBeDefined(); }, - 60000 + 60000, ); it.concurrent( @@ -293,7 +293,7 @@ describe("E2E Tests for Extract API Routes", () => { .set("Content-Type", "application/json") .send({ urls: ["https://docs.firecrawl.dev"], - prompt: "What is the title and description of the page?" + prompt: "What is the title and description of the page?", }); console.log(response.body.data); @@ -302,6 +302,6 @@ describe("E2E Tests for Extract API Routes", () => { expect(typeof response.body.data).toBe("object"); expect(Object.keys(response.body.data).length).toBeGreaterThan(0); }, - 60000 + 60000, ); }); diff --git a/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts b/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts index a8841aab..45b3c31e 100644 --- a/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_full_withAuth/index.test.ts @@ -47,7 +47,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent("should return an error for a blocklisted URL", async () => { @@ -59,7 +59,7 @@ describe("E2E Tests for API Routes", () => { .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); expect(response.body.error).toContain( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); @@ -103,30 +103,30 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data.metadata.pageError).toBeUndefined(); expect(response.body.data.metadata.title).toBe("Roast My Website"); expect(response.body.data.metadata.description).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(response.body.data.metadata.keywords).toBe( - "Roast My Website,Roast,Website,GitHub,Firecrawl" + "Roast My Website,Roast,Website,GitHub,Firecrawl", ); expect(response.body.data.metadata.robots).toBe("follow, index"); expect(response.body.data.metadata.ogTitle).toBe("Roast My Website"); expect(response.body.data.metadata.ogDescription).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(response.body.data.metadata.ogUrl).toBe( - "https://www.roastmywebsite.ai" + "https://www.roastmywebsite.ai", ); expect(response.body.data.metadata.ogImage).toBe( - "https://www.roastmywebsite.ai/og.png" + "https://www.roastmywebsite.ai/og.png", ); expect(response.body.data.metadata.ogLocaleAlternate).toStrictEqual([]); expect(response.body.data.metadata.ogSiteName).toBe("Roast My Website"); expect(response.body.data.metadata.sourceURL).toBe( - "https://roastmywebsite.ai" + "https://roastmywebsite.ai", ); expect(response.body.data.metadata.pageStatusCode).toBe(200); }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( @@ -138,7 +138,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true } + pageOptions: { includeHtml: true }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -152,7 +152,7 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( @@ -164,7 +164,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://roastmywebsite.ai", - pageOptions: { includeRawHtml: true } + pageOptions: { includeRawHtml: true }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -178,7 +178,7 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( @@ -196,12 +196,12 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("content"); expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.content).toContain( - "We present spectrophotometric observations of the Broad Line Radio Galaxy" + "We present spectrophotometric observations of the Broad Line Radio Galaxy", ); expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -219,12 +219,12 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("content"); expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.content).toContain( - "We present spectrophotometric observations of the Broad Line Radio Galaxy" + "We present spectrophotometric observations of the Broad Line Radio Galaxy", ); expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -236,7 +236,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://arxiv.org/pdf/astro-ph/9301001.pdf", - pageOptions: { parsePDF: false } + pageOptions: { parsePDF: false }, }); await new Promise((r) => setTimeout(r, 6000)); @@ -245,10 +245,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("content"); expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.content).toContain( - "/Title(arXiv:astro-ph/9301001v1 7 Jan 1993)>>endobj" + "/Title(arXiv:astro-ph/9301001v1 7 Jan 1993)>>endobj", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -266,16 +266,16 @@ describe("E2E Tests for API Routes", () => { expect(responseWithoutRemoveTags.body.data).toHaveProperty("metadata"); expect(responseWithoutRemoveTags.body.data).not.toHaveProperty("html"); expect(responseWithoutRemoveTags.body.data.content).toContain( - "Scrape This Site" + "Scrape This Site", ); expect(responseWithoutRemoveTags.body.data.content).toContain( - "Lessons and Videos" + "Lessons and Videos", ); // #footer expect(responseWithoutRemoveTags.body.data.content).toContain( - "[Sandbox](" + "[Sandbox](", ); // .nav expect(responseWithoutRemoveTags.body.data.content).toContain( - "web scraping" + "web scraping", ); // strong const response = await request(TEST_URL) @@ -284,7 +284,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com/", - pageOptions: { removeTags: [".nav", "#footer", "strong"] } + pageOptions: { removeTags: [".nav", "#footer", "strong"] }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -297,7 +297,7 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data.content).not.toContain("[Sandbox]("); // .nav expect(response.body.data.content).not.toContain("web scraping"); // strong }, - 30000 + 30000, ); // 30 seconds timeout // TODO: add this test back once we nail the waitFor option to be more deterministic @@ -337,10 +337,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(400); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "bad request" + "bad request", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -359,10 +359,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(401); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "unauthorized" + "unauthorized", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -381,10 +381,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(403); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "forbidden" + "forbidden", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -403,10 +403,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(404); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "not found" + "not found", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -425,10 +425,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(405); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "method not allowed" + "method not allowed", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -447,10 +447,10 @@ describe("E2E Tests for API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(500); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "internal server error" + "internal server error", ); }, - 60000 + 60000, ); // 60 seconds }); @@ -469,7 +469,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent("should return an error for a blocklisted URL", async () => { @@ -481,7 +481,7 @@ describe("E2E Tests for API Routes", () => { .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); expect(response.body.error).toContain( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); @@ -496,9 +496,9 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("jobId"); expect(response.body.jobId).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/, ); - } + }, ); it.concurrent( "should prevent duplicate requests using the same idempotency key", @@ -525,7 +525,7 @@ describe("E2E Tests for API Routes", () => { expect(secondResponse.statusCode).toBe(409); expect(secondResponse.body.error).toBe("Idempotency key already used"); - } + }, ); it.concurrent( @@ -539,8 +539,8 @@ describe("E2E Tests for API Routes", () => { url: "https://mendable.ai", limit: 10, crawlerOptions: { - includes: ["blog/*"] - } + includes: ["blog/*"], + }, }); let response; @@ -563,7 +563,7 @@ describe("E2E Tests for API Routes", () => { const completedResponse = response; const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(5); urls.forEach((url: string) => { @@ -579,13 +579,13 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].content).toContain("Mendable"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -599,8 +599,8 @@ describe("E2E Tests for API Routes", () => { url: "https://mendable.ai", limit: 10, crawlerOptions: { - excludes: ["blog/*"] - } + excludes: ["blog/*"], + }, }); let isFinished = false; @@ -623,14 +623,14 @@ describe("E2E Tests for API Routes", () => { const completedResponse = response; const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(5); urls.forEach((url: string) => { expect(url.startsWith("https://wwww.mendable.ai/blog/")).toBeFalsy(); }); }, - 90000 + 90000, ); // 90 seconds it.concurrent( @@ -642,7 +642,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://mendable.ai", - crawlerOptions: { limit: 3 } + crawlerOptions: { limit: 3 }, }); let isFinished = false; @@ -674,13 +674,13 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].content).toContain("Mendable"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -692,7 +692,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com", - crawlerOptions: { maxDepth: 1 } + crawlerOptions: { maxDepth: 1 }, }); expect(crawlResponse.statusCode).toBe(200); @@ -726,13 +726,13 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(1); @@ -748,7 +748,7 @@ describe("E2E Tests for API Routes", () => { expect(depth).toBeLessThanOrEqual(2); }); }, - 180000 + 180000, ); it.concurrent( @@ -760,7 +760,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com/pages/", - crawlerOptions: { maxDepth: 1 } + crawlerOptions: { maxDepth: 1 }, }); expect(crawlResponse.statusCode).toBe(200); @@ -794,7 +794,7 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(1); @@ -810,7 +810,7 @@ describe("E2E Tests for API Routes", () => { expect(depth).toBeLessThanOrEqual(3); }); }, - 180000 + 180000, ); it.concurrent( @@ -822,7 +822,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.mendable.ai", - crawlerOptions: { maxDepth: 0 } + crawlerOptions: { maxDepth: 0 }, }); expect(crawlResponse.statusCode).toBe(200); @@ -849,7 +849,7 @@ describe("E2E Tests for API Routes", () => { .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); const testurls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); //console.log(testurls) @@ -861,7 +861,7 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThanOrEqual(1); @@ -877,7 +877,7 @@ describe("E2E Tests for API Routes", () => { expect(depth).toBeLessThanOrEqual(1); }); }, - 180000 + 180000, ); // it.concurrent("should return a successful response with a valid API key and valid limit option", async () => { @@ -934,7 +934,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true } + pageOptions: { includeHtml: true }, }); expect(crawlResponse.statusCode).toBe(200); @@ -969,10 +969,10 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); // 120 seconds @@ -983,13 +983,13 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0].html).toContain(" { allowExternalContentLinks: true, ignoreSitemap: true, returnOnlyUrls: true, - limit: 50 - } + limit: 50, + }, }); expect(crawlInitResponse.statusCode).toBe(200); @@ -1031,19 +1031,19 @@ describe("E2E Tests for API Routes", () => { expect.arrayContaining([ expect.objectContaining({ url: expect.stringContaining( - "https://firecrawl.dev/?ref=mendable+banner" - ) + "https://firecrawl.dev/?ref=mendable+banner", + ), }), expect.objectContaining({ - url: expect.stringContaining("https://mendable.ai/pricing") + url: expect.stringContaining("https://mendable.ai/pricing"), }), expect.objectContaining({ - url: expect.stringContaining("https://x.com/CalebPeffer") - }) - ]) + url: expect.stringContaining("https://x.com/CalebPeffer"), + }), + ]), ); }, - 180000 + 180000, ); // 3 minutes timeout }); @@ -1062,7 +1062,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); // it.concurrent("should return an error for a blocklisted URL", async () => { @@ -1088,7 +1088,7 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(408); }, - 3000 + 3000, ); // it.concurrent("should return a successful response with a valid API key for crawlWebsitePreview", async () => { @@ -1120,7 +1120,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ query: "test" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -1136,7 +1136,7 @@ describe("E2E Tests for API Routes", () => { expect(response.body.success).toBe(true); expect(response.body).toHaveProperty("data"); }, - 30000 + 30000, ); // 30 seconds timeout }); @@ -1153,7 +1153,7 @@ describe("E2E Tests for API Routes", () => { .get("/v0/crawl/status/123") .set("Authorization", `Bearer invalid-api-key`); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -1163,7 +1163,7 @@ describe("E2E Tests for API Routes", () => { .get("/v0/crawl/status/invalidJobId") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); expect(response.statusCode).toBe(404); - } + }, ); it.concurrent( @@ -1201,22 +1201,22 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].content).toContain("Mendable"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); const childrenLinks = completedResponse.body.data.filter( (doc) => doc.metadata && doc.metadata.sourceURL && - doc.metadata.sourceURL.includes("mendable.ai/blog") + doc.metadata.sourceURL.includes("mendable.ai/blog"), ); expect(childrenLinks.length).toBe(completedResponse.body.data.length); }, - 180000 + 180000, ); // 120 seconds it.concurrent( @@ -1236,9 +1236,9 @@ describe("E2E Tests for API Routes", () => { "abs/*", "static/*", "about/*", - "archive/*" - ] - } + "archive/*", + ], + }, }); expect(crawlResponse.statusCode).toBe(200); @@ -1266,21 +1266,21 @@ describe("E2E Tests for API Routes", () => { expect.arrayContaining([ expect.objectContaining({ content: expect.stringContaining( - "asymmetries might represent, for instance, preferred source orientations to our line of sight." - ) - }) - ]) + "asymmetries might represent, for instance, preferred source orientations to our line of sight.", + ), + }), + ]), ); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); }, - 180000 + 180000, ); // 120 seconds it.concurrent( @@ -1292,7 +1292,7 @@ describe("E2E Tests for API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true } + pageOptions: { includeHtml: true }, }); expect(crawlResponse.statusCode).toBe(200); @@ -1333,13 +1333,13 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.data[0].markdown).toContain("_Roast_"); expect(completedResponse.body.data[0].html).toContain(" { .send({ url: "https://mendable.ai/blog", pageOptions: { includeHtml: true }, - crawlerOptions: { allowBackwardCrawling: true } + crawlerOptions: { allowBackwardCrawling: true }, }); expect(crawlResponse.statusCode).toBe(200); @@ -1397,10 +1397,10 @@ describe("E2E Tests for API Routes", () => { }); expect(completedResponse.body.data.length).toBeGreaterThan( - onlyChildrenLinks.length + onlyChildrenLinks.length, ); }, - 60000 + 60000, ); it.concurrent( @@ -1438,13 +1438,13 @@ describe("E2E Tests for API Routes", () => { expect(completedResponse.body.partial_data[0]).toHaveProperty("markdown"); expect(completedResponse.body.partial_data[0]).toHaveProperty("metadata"); expect( - completedResponse.body.partial_data[0].metadata.pageStatusCode + completedResponse.body.partial_data[0].metadata.pageStatusCode, ).toBe(200); expect( - completedResponse.body.partial_data[0].metadata.pageError + completedResponse.body.partial_data[0].metadata.pageError, ).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds describe("POST /v0/scrape with LLM Extraction", () => { @@ -1458,7 +1458,7 @@ describe("E2E Tests for API Routes", () => { .send({ url: "https://mendable.ai", pageOptions: { - onlyMainContent: true + onlyMainContent: true, }, extractorOptions: { mode: "llm-extraction", @@ -1468,18 +1468,18 @@ describe("E2E Tests for API Routes", () => { type: "object", properties: { company_mission: { - type: "string" + type: "string", }, supports_sso: { - type: "boolean" + type: "boolean", }, is_open_source: { - type: "boolean" - } + type: "boolean", + }, }, - required: ["company_mission", "supports_sso", "is_open_source"] - } - } + required: ["company_mission", "supports_sso", "is_open_source"], + }, + }, }); // Ensure that the job was successfully created before proceeding with LLM extraction @@ -1498,7 +1498,7 @@ describe("E2E Tests for API Routes", () => { expect(llmExtraction.is_open_source).toBe(false); expect(typeof llmExtraction.is_open_source).toBe("boolean"); }, - 60000 + 60000, ); // 60 secs it.concurrent( @@ -1519,15 +1519,15 @@ describe("E2E Tests for API Routes", () => { type: "object", properties: { primary_cta: { - type: "string" + type: "string", }, secondary_cta: { - type: "string" - } + type: "string", + }, }, - required: ["primary_cta", "secondary_cta"] - } - } + required: ["primary_cta", "secondary_cta"], + }, + }, }); // Ensure that the job was successfully created before proceeding with LLM extraction @@ -1542,7 +1542,7 @@ describe("E2E Tests for API Routes", () => { expect(llmExtraction).toHaveProperty("secondary_cta"); expect(typeof llmExtraction.secondary_cta).toBe("string"); }, - 60000 + 60000, ); // 60 secs }); @@ -1617,8 +1617,8 @@ describe("E2E Tests for API Routes", () => { .send({ url: "https://flutterbricks.com", crawlerOptions: { - mode: "fast" - } + mode: "fast", + }, }); expect(crawlResponse.statusCode).toBe(200); @@ -1660,7 +1660,7 @@ describe("E2E Tests for API Routes", () => { expect(results.length).toBeGreaterThanOrEqual(10); expect(results.length).toBeLessThanOrEqual(15); }, - 20000 + 20000, ); // it.concurrent("should complete the crawl in more than 10 seconds", async () => { @@ -1741,7 +1741,7 @@ describe("E2E Tests for API Routes", () => { expect(response.statusCode).toBe(429); }, - 90000 + 90000, ); }); diff --git a/apps/api/src/__tests__/e2e_map/index.test.ts b/apps/api/src/__tests__/e2e_map/index.test.ts index 948f097e..30ec6776 100644 --- a/apps/api/src/__tests__/e2e_map/index.test.ts +++ b/apps/api/src/__tests__/e2e_map/index.test.ts @@ -15,7 +15,7 @@ describe("E2E Tests for Map API Routes", () => { .send({ url: "https://firecrawl.dev", sitemapOnly: false, - search: "smart-crawl" + search: "smart-crawl", }); console.log(response.body); @@ -24,7 +24,7 @@ describe("E2E Tests for Map API Routes", () => { expect(response.body.links.length).toBeGreaterThan(0); expect(response.body.links[0]).toContain("firecrawl.dev/smart-crawl"); }, - 60000 + 60000, ); it.concurrent( @@ -37,7 +37,7 @@ describe("E2E Tests for Map API Routes", () => { .send({ url: "https://firecrawl.dev", sitemapOnly: false, - includeSubdomains: true + includeSubdomains: true, }); console.log(response.body); @@ -45,10 +45,10 @@ describe("E2E Tests for Map API Routes", () => { expect(response.body).toHaveProperty("links"); expect(response.body.links.length).toBeGreaterThan(0); expect(response.body.links[response.body.links.length - 1]).toContain( - "docs.firecrawl.dev" + "docs.firecrawl.dev", ); }, - 60000 + 60000, ); it.concurrent( @@ -60,7 +60,7 @@ describe("E2E Tests for Map API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev", - sitemapOnly: true + sitemapOnly: true, }); console.log(response.body); @@ -68,10 +68,10 @@ describe("E2E Tests for Map API Routes", () => { expect(response.body).toHaveProperty("links"); expect(response.body.links.length).toBeGreaterThan(0); expect(response.body.links[response.body.links.length - 1]).not.toContain( - "docs.firecrawl.dev" + "docs.firecrawl.dev", ); }, - 60000 + 60000, ); it.concurrent( @@ -84,7 +84,7 @@ describe("E2E Tests for Map API Routes", () => { .send({ url: "https://firecrawl.dev", sitemapOnly: false, - limit: 10 + limit: 10, }); console.log(response.body); @@ -92,7 +92,7 @@ describe("E2E Tests for Map API Routes", () => { expect(response.body).toHaveProperty("links"); expect(response.body.links.length).toBeLessThanOrEqual(10); }, - 60000 + 60000, ); it.concurrent( @@ -104,7 +104,7 @@ describe("E2E Tests for Map API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://geekflare.com/sitemap_index.xml", - sitemapOnly: true + sitemapOnly: true, }); console.log(response.body); @@ -112,6 +112,6 @@ describe("E2E Tests for Map API Routes", () => { expect(response.body).toHaveProperty("links"); expect(response.body.links.length).toBeGreaterThan(1900); }, - 60000 + 60000, ); }); diff --git a/apps/api/src/__tests__/e2e_noAuth/index.test.ts b/apps/api/src/__tests__/e2e_noAuth/index.test.ts index 9c3ddf33..e30352a5 100644 --- a/apps/api/src/__tests__/e2e_noAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_noAuth/index.test.ts @@ -62,7 +62,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); expect(response.body.error).toContain( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); @@ -89,7 +89,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); expect(response.body.error).toContain( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); @@ -101,7 +101,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("jobId"); expect(response.body.jobId).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/, ); }); }); @@ -120,7 +120,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { .send({ url: blocklistedUrl }); expect(response.statusCode).toBe(403); expect(response.body.error).toContain( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); @@ -132,7 +132,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("jobId"); expect(response.body.jobId).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/, ); }); }); @@ -172,7 +172,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { it("should return Job not found for invalid job ID", async () => { const response = await request(TEST_URL).get( - "/v0/crawl/status/invalidJobId" + "/v0/crawl/status/invalidJobId", ); expect(response.statusCode).toBe(404); }); @@ -185,7 +185,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { expect(crawlResponse.statusCode).toBe(200); const response = await request(TEST_URL).get( - `/v0/crawl/status/${crawlResponse.body.jobId}` + `/v0/crawl/status/${crawlResponse.body.jobId}`, ); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("status"); @@ -195,7 +195,7 @@ describe("E2E Tests for API Routes with No Authentication", () => { await new Promise((r) => setTimeout(r, 30000)); const completedResponse = await request(TEST_URL).get( - `/v0/crawl/status/${crawlResponse.body.jobId}` + `/v0/crawl/status/${crawlResponse.body.jobId}`, ); expect(completedResponse.statusCode).toBe(200); expect(completedResponse.body).toHaveProperty("status"); diff --git a/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts b/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts index 33e3be5d..35ee2d89 100644 --- a/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_v1_withAuth/index.test.ts @@ -2,7 +2,7 @@ import request from "supertest"; import { configDotenv } from "dotenv"; import { ScrapeRequestInput, - ScrapeResponseRequestTest + ScrapeResponseRequestTest, } from "../../controllers/v1/types"; configDotenv(); @@ -24,7 +24,7 @@ describe("E2E Tests for v1 API Routes", () => { console.log( "process.env.USE_DB_AUTHENTICATION", - process.env.USE_DB_AUTHENTICATION + process.env.USE_DB_AUTHENTICATION, ); console.log("?", process.env.USE_DB_AUTHENTICATION === "true"); const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; @@ -47,7 +47,7 @@ describe("E2E Tests for v1 API Routes", () => { it.concurrent("should throw error for blocklisted URL", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://facebook.com/fake-test" + url: "https://facebook.com/fake-test", }; const response = await request(TEST_URL) @@ -58,7 +58,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.statusCode).toBe(403); expect(response.body.error).toBe( - "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions." + "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions.", ); }); @@ -71,14 +71,14 @@ describe("E2E Tests for v1 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( "should return a successful response with a valid API key", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://roastmywebsite.ai" + url: "https://roastmywebsite.ai", }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -100,37 +100,37 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.metadata.error).toBeUndefined(); expect(response.body.data.metadata.title).toBe("Roast My Website"); expect(response.body.data.metadata.description).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(response.body.data.metadata.keywords).toBe( - "Roast My Website,Roast,Website,GitHub,Firecrawl" + "Roast My Website,Roast,Website,GitHub,Firecrawl", ); expect(response.body.data.metadata.robots).toBe("follow, index"); expect(response.body.data.metadata.ogTitle).toBe("Roast My Website"); expect(response.body.data.metadata.ogDescription).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(response.body.data.metadata.ogUrl).toBe( - "https://www.roastmywebsite.ai" + "https://www.roastmywebsite.ai", ); expect(response.body.data.metadata.ogImage).toBe( - "https://www.roastmywebsite.ai/og.png" + "https://www.roastmywebsite.ai/og.png", ); expect(response.body.data.metadata.ogLocaleAlternate).toStrictEqual([]); expect(response.body.data.metadata.ogSiteName).toBe("Roast My Website"); expect(response.body.data.metadata.sourceURL).toBe( - "https://roastmywebsite.ai" + "https://roastmywebsite.ai", ); expect(response.body.data.metadata.statusCode).toBe(200); }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( "should return a successful response with a valid API key", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://arxiv.org/abs/2410.04840" + url: "https://arxiv.org/abs/2410.04840", }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -151,43 +151,43 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.markdown).toContain("Strong Model Collapse"); expect(response.body.data.metadata.error).toBeUndefined(); expect(response.body.data.metadata.description).toContain( - "Abstract page for arXiv paper 2410.04840: Strong Model Collapse" + "Abstract page for arXiv paper 2410.04840: Strong Model Collapse", ); expect(response.body.data.metadata.citation_title).toBe( - "Strong Model Collapse" + "Strong Model Collapse", ); expect(response.body.data.metadata.citation_author).toEqual([ "Dohmatob, Elvis", "Feng, Yunzhen", "Subramonian, Arjun", - "Kempe, Julia" + "Kempe, Julia", ]); expect(response.body.data.metadata.citation_date).toBe("2024/10/07"); expect(response.body.data.metadata.citation_online_date).toBe( - "2024/10/08" + "2024/10/08", ); expect(response.body.data.metadata.citation_pdf_url).toBe( - "http://arxiv.org/pdf/2410.04840" + "http://arxiv.org/pdf/2410.04840", ); expect(response.body.data.metadata.citation_arxiv_id).toBe( - "2410.04840" + "2410.04840", ); expect(response.body.data.metadata.citation_abstract).toContain( - "Within the scaling laws paradigm" + "Within the scaling laws paradigm", ); expect(response.body.data.metadata.sourceURL).toBe( - "https://arxiv.org/abs/2410.04840" + "https://arxiv.org/abs/2410.04840", ); expect(response.body.data.metadata.statusCode).toBe(200); }, - 30000 + 30000, ); it.concurrent( "should return a successful response with a valid API key and includeHtml set to true", async () => { const scrapeRequest: ScrapeRequestInput = { url: "https://roastmywebsite.ai", - formats: ["markdown", "html"] + formats: ["markdown", "html"], }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -209,13 +209,13 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.metadata.statusCode).toBe(200); expect(response.body.data.metadata.error).toBeUndefined(); }, - 30000 + 30000, ); it.concurrent( "should return a successful response for a valid scrape with PDF file", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://arxiv.org/pdf/astro-ph/9301001.pdf" + url: "https://arxiv.org/pdf/astro-ph/9301001.pdf", // formats: ["markdown", "html"], }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -232,19 +232,19 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.markdown).toContain( - "Broad Line Radio Galaxy" + "Broad Line Radio Galaxy", ); expect(response.body.data.metadata.statusCode).toBe(200); expect(response.body.data.metadata.error).toBeUndefined(); }, - 60000 + 60000, ); it.concurrent( "should return a successful response for a valid scrape with PDF file without explicit .pdf extension", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://arxiv.org/pdf/astro-ph/9301001" + url: "https://arxiv.org/pdf/astro-ph/9301001", }; const response: ScrapeResponseRequestTest = await request(TEST_URL) .post("/v1/scrape") @@ -261,12 +261,12 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("markdown"); expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.markdown).toContain( - "Broad Line Radio Galaxy" + "Broad Line Radio Galaxy", ); expect(response.body.data.metadata.statusCode).toBe(200); expect(response.body.data.metadata.error).toBeUndefined(); }, - 60000 + 60000, ); it.concurrent( @@ -274,7 +274,7 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest: ScrapeRequestInput = { url: "https://www.scrapethissite.com/", - onlyMainContent: false // default is true + onlyMainContent: false, // default is true }; const responseWithoutRemoveTags: ScrapeResponseRequestTest = await request(TEST_URL) @@ -292,16 +292,16 @@ describe("E2E Tests for v1 API Routes", () => { expect(responseWithoutRemoveTags.body.data).toHaveProperty("metadata"); expect(responseWithoutRemoveTags.body.data).not.toHaveProperty("html"); expect(responseWithoutRemoveTags.body.data.markdown).toContain( - "[FAQ](/faq/)" + "[FAQ](/faq/)", ); // .nav expect(responseWithoutRemoveTags.body.data.markdown).toContain( - "Hartley Brody 2023" + "Hartley Brody 2023", ); // #footer const scrapeRequestWithRemoveTags: ScrapeRequestInput = { url: "https://www.scrapethissite.com/", excludeTags: [".nav", "#footer", "strong"], - onlyMainContent: false // default is true + onlyMainContent: false, // default is true }; const response: ScrapeResponseRequestTest = await request(TEST_URL) .post("/v1/scrape") @@ -320,7 +320,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.markdown).not.toContain("Hartley Brody 2023"); expect(response.body.data.markdown).not.toContain("[FAQ](/faq/)"); // }, - 30000 + 30000, ); it.concurrent( @@ -342,7 +342,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(400); }, - 60000 + 60000, ); it.concurrent( @@ -364,7 +364,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(401); }, - 60000 + 60000, ); // Removed it as we want to retry fallback to the next scraper @@ -405,7 +405,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(404); }, - 60000 + 60000, ); // it.concurrent('should return a successful response for a scrape with 405 page', async () => { @@ -455,7 +455,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.statusCode).toBe(408); }, - 3000 + 3000, ); it.concurrent( @@ -463,7 +463,7 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest: ScrapeRequestInput = { url: "https://roastmywebsite.ai", - formats: ["html", "rawHtml"] + formats: ["html", "rawHtml"], }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -486,7 +486,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.metadata.statusCode).toBe(200); expect(response.body.data.metadata.error).toBeUndefined(); }, - 30000 + 30000, ); it.concurrent( @@ -495,7 +495,7 @@ describe("E2E Tests for v1 API Routes", () => { const scrapeRequest: ScrapeRequestInput = { url: "https://ycombinator.com/companies", formats: ["markdown"], - waitFor: 8000 + waitFor: 8000, }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -518,7 +518,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.metadata.statusCode).toBe(200); expect(response.body.data.metadata.error).toBeUndefined(); }, - 30000 + 30000, ); it.concurrent( @@ -526,7 +526,7 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest: ScrapeRequestInput = { url: "https://roastmywebsite.ai", - formats: ["links"] + formats: ["links"], }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -548,7 +548,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data.metadata.statusCode).toBe(200); expect(response.body.data.metadata.error).toBeUndefined(); }, - 30000 + 30000, ); }); @@ -569,14 +569,14 @@ describe("E2E Tests for v1 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( "should return a successful response with a valid API key", async () => { const mapRequest = { - url: "https://roastmywebsite.ai" + url: "https://roastmywebsite.ai", }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -594,7 +594,7 @@ describe("E2E Tests for v1 API Routes", () => { const links = response.body.links as unknown[]; expect(Array.isArray(links)).toBe(true); expect(links.length).toBeGreaterThan(0); - } + }, ); it.concurrent( @@ -602,7 +602,7 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const mapRequest = { url: "https://usemotion.com", - search: "pricing" + search: "pricing", }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -621,7 +621,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(Array.isArray(links)).toBe(true); expect(links.length).toBeGreaterThan(0); expect(links[0]).toContain("usemotion.com/pricing"); - } + }, ); it.concurrent( @@ -630,7 +630,7 @@ describe("E2E Tests for v1 API Routes", () => { const mapRequest = { url: "https://firecrawl.dev", search: "docs", - includeSubdomains: true + includeSubdomains: true, }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -650,10 +650,10 @@ describe("E2E Tests for v1 API Routes", () => { expect(links.length).toBeGreaterThan(0); const containsDocsFirecrawlDev = links.some((link: string) => - link.includes("docs.firecrawl.dev") + link.includes("docs.firecrawl.dev"), ); expect(containsDocsFirecrawlDev).toBe(true); - } + }, ); it.concurrent( @@ -662,7 +662,7 @@ describe("E2E Tests for v1 API Routes", () => { const mapRequest = { url: "https://www.firecrawl.dev", search: "docs", - includeSubdomains: true + includeSubdomains: true, }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -682,11 +682,11 @@ describe("E2E Tests for v1 API Routes", () => { expect(links.length).toBeGreaterThan(0); const containsDocsFirecrawlDev = links.some((link: string) => - link.includes("docs.firecrawl.dev") + link.includes("docs.firecrawl.dev"), ); expect(containsDocsFirecrawlDev).toBe(true); }, - 10000 + 10000, ); it.concurrent( @@ -695,7 +695,7 @@ describe("E2E Tests for v1 API Routes", () => { const mapRequest = { url: "https://www.firecrawl.dev", search: "docs", - includeSubdomains: false + includeSubdomains: false, }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -714,14 +714,14 @@ describe("E2E Tests for v1 API Routes", () => { expect(Array.isArray(links)).toBe(true); expect(links.length).toBeGreaterThan(0); expect(links[0]).not.toContain("docs.firecrawl.dev"); - } + }, ); it.concurrent("should return an error for invalid URL", async () => { const mapRequest = { url: "invalid-url", includeSubdomains: true, - search: "test" + search: "test", }; const response: ScrapeResponseRequestTest = await request(TEST_URL) @@ -746,7 +746,7 @@ describe("E2E Tests for v1 API Routes", () => { it.concurrent("should throw error for blocklisted URL", async () => { const scrapeRequest: ScrapeRequestInput = { - url: "https://facebook.com/fake-test" + url: "https://facebook.com/fake-test", }; const response = await request(TEST_URL) @@ -757,7 +757,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.statusCode).toBe(403); expect(response.body.error).toBe( - "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions." + "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions.", ); }); @@ -770,7 +770,7 @@ describe("E2E Tests for v1 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent("should return a successful response", async () => { @@ -783,7 +783,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("id"); expect(response.body.id).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/, ); expect(response.body).toHaveProperty("success", true); expect(response.body).toHaveProperty("url"); @@ -800,7 +800,7 @@ describe("E2E Tests for v1 API Routes", () => { .send({ url: "https://firecrawl.dev", limit: 40, - includePaths: ["blog/*"] + includePaths: ["blog/*"], }); let response; @@ -826,7 +826,7 @@ describe("E2E Tests for v1 API Routes", () => { .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(5); urls.forEach((url: string) => { @@ -843,7 +843,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); }, - 180000 + 180000, ); // 180 seconds it.concurrent( @@ -856,7 +856,7 @@ describe("E2E Tests for v1 API Routes", () => { .send({ url: "https://firecrawl.dev", limit: 40, - excludePaths: ["blog/*"] + excludePaths: ["blog/*"], }); let isFinished = false; @@ -882,14 +882,14 @@ describe("E2E Tests for v1 API Routes", () => { .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(3); urls.forEach((url: string) => { expect(url.startsWith("https://www.firecrawl.dev/blog/")).toBeFalsy(); }); }, - 90000 + 90000, ); // 90 seconds it.concurrent( @@ -901,7 +901,7 @@ describe("E2E Tests for v1 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com", - maxDepth: 1 + maxDepth: 1, }); expect(crawlResponse.statusCode).toBe(200); @@ -911,7 +911,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("status"); expect(["active", "waiting", "completed", "scraping"]).toContain( - response.body.status + response.body.status, ); // wait for 60 seconds let isCompleted = false; @@ -939,7 +939,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(1); @@ -955,7 +955,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(depth).toBeLessThanOrEqual(2); }); }, - 180000 + 180000, ); }); @@ -972,7 +972,7 @@ describe("E2E Tests for v1 API Routes", () => { .get("/v1/crawl/123") .set("Authorization", `Bearer invalid-api-key`); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -982,7 +982,7 @@ describe("E2E Tests for v1 API Routes", () => { .get("/v1/crawl/invalidJobId") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); expect(response.statusCode).toBe(404); - } + }, ); it.concurrent( @@ -1026,12 +1026,12 @@ describe("E2E Tests for v1 API Routes", () => { expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); const childrenLinks = completedResponse.body.data.filter( - (doc) => doc.metadata && doc.metadata.sourceURL + (doc) => doc.metadata && doc.metadata.sourceURL, ); expect(childrenLinks.length).toBe(completedResponse.body.data.length); }, - 180000 + 180000, ); // 120 seconds it.concurrent( @@ -1068,7 +1068,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(completedResponse.body.data[0].metadata.statusCode).toBe(200); expect(completedResponse.body.data[0].metadata.error).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds }); }); diff --git a/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts b/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts index e297f7c8..313b7357 100644 --- a/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts +++ b/apps/api/src/__tests__/e2e_v1_withAuth_all_params/index.test.ts @@ -2,7 +2,7 @@ import request from "supertest"; import { configDotenv } from "dotenv"; import { ScrapeRequest, - ScrapeResponseRequestTest + ScrapeResponseRequestTest, } from "../../controllers/v1/types"; configDotenv(); @@ -14,7 +14,7 @@ describe("E2E Tests for v1 API Routes", () => { "should return a successful response for a scrape with 403 page", async () => { const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -30,18 +30,18 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.statusCode).toBe(403); }, - 30000 + 30000, ); it.concurrent( "should handle 'formats:markdown (default)' parameter correctly", async () => { const scrapeRequest = { - url: E2E_TEST_SERVER_URL + url: E2E_TEST_SERVER_URL, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -57,26 +57,26 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("markdown"); expect(response.body.data.markdown).toContain( - "This page is used for end-to-end (e2e) testing with Firecrawl." + "This page is used for end-to-end (e2e) testing with Firecrawl.", ); expect(response.body.data.markdown).toContain( - "Content with id #content-1" + "Content with id #content-1", ); // expect(response.body.data.markdown).toContain("Loading..."); expect(response.body.data.markdown).toContain("Click me!"); expect(response.body.data.markdown).toContain( - "Power your AI apps with clean data crawled from any website. It's also open-source." + "Power your AI apps with clean data crawled from any website. It's also open-source.", ); // firecrawl.dev inside an iframe expect(response.body.data.markdown).toContain( - "This content loads only when you see it. Don't blink! 👼" + "This content loads only when you see it. Don't blink! 👼", ); // the browser always scroll to the bottom expect(response.body.data.markdown).not.toContain("Header"); // Only main content is returned by default expect(response.body.data.markdown).not.toContain("footer"); // Only main content is returned by default expect(response.body.data.markdown).not.toContain( - "This content is only visible on mobile" + "This content is only visible on mobile", ); }, - 30000 + 30000, ); it.concurrent( @@ -84,11 +84,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - formats: ["html"] + formats: ["html"], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -105,13 +105,13 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("html"); expect(response.body.data.html).not.toContain( - '
Header
' + '
Header
', ); expect(response.body.data.html).toContain( - '

This page is used for end-to-end (e2e) testing with Firecrawl.

' + '

This page is used for end-to-end (e2e) testing with Firecrawl.

', ); }, - 30000 + 30000, ); it.concurrent( @@ -119,11 +119,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - formats: ["rawHtml"] + formats: ["rawHtml"], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -140,11 +140,11 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.data).toHaveProperty("rawHtml"); expect(response.body.data.rawHtml).toContain( - ">This page is used for end-to-end (e2e) testing with Firecrawl.

" + ">This page is used for end-to-end (e2e) testing with Firecrawl.

", ); expect(response.body.data.rawHtml).toContain(">Header"); }, - 30000 + 30000, ); // - TODO: tests for links @@ -157,11 +157,11 @@ describe("E2E Tests for v1 API Routes", () => { // @ts-ignore const scrapeRequest = { url: E2E_TEST_SERVER_URL, - headers: { "e2e-header-test": "firecrawl" } + headers: { "e2e-header-test": "firecrawl" }, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -175,10 +175,10 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).toContain( - "e2e-header-test: firecrawl" + "e2e-header-test: firecrawl", ); }, - 30000 + 30000, ); it.concurrent( @@ -186,11 +186,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - includeTags: ["#content-1"] + includeTags: ["#content-1"], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -204,13 +204,13 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).not.toContain( - "

This page is used for end-to-end (e2e) testing with Firecrawl.

" + "

This page is used for end-to-end (e2e) testing with Firecrawl.

", ); expect(response.body.data.markdown).toContain( - "Content with id #content-1" + "Content with id #content-1", ); }, - 30000 + 30000, ); it.concurrent( @@ -218,11 +218,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - excludeTags: ["#content-1"] + excludeTags: ["#content-1"], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -236,13 +236,13 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).toContain( - "This page is used for end-to-end (e2e) testing with Firecrawl." + "This page is used for end-to-end (e2e) testing with Firecrawl.", ); expect(response.body.data.markdown).not.toContain( - "Content with id #content-1" + "Content with id #content-1", ); }, - 30000 + 30000, ); it.concurrent( @@ -251,11 +251,11 @@ describe("E2E Tests for v1 API Routes", () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, formats: ["html", "markdown"], - onlyMainContent: false + onlyMainContent: false, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -269,13 +269,13 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).toContain( - "This page is used for end-to-end (e2e) testing with Firecrawl." + "This page is used for end-to-end (e2e) testing with Firecrawl.", ); expect(response.body.data.html).toContain( - '
Header
' + '
Header
', ); }, - 30000 + 30000, ); it.concurrent( @@ -283,11 +283,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - timeout: 500 + timeout: 500, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -302,7 +302,7 @@ describe("E2E Tests for v1 API Routes", () => { expect(response.body.error).toBe("Request timed out"); expect(response.body.success).toBe(false); }, - 30000 + 30000, ); it.concurrent( @@ -310,11 +310,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - mobile: true + mobile: true, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -327,17 +327,17 @@ describe("E2E Tests for v1 API Routes", () => { throw new Error("Expected response body to have 'data' property"); } expect(response.body.data.markdown).toContain( - "This content is only visible on mobile" + "This content is only visible on mobile", ); }, - 30000 + 30000, ); it.concurrent( "should handle 'parsePDF' parameter correctly", async () => { const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -352,21 +352,21 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).toContain( - "arXiv:astro-ph/9301001v1 7 Jan 1993" + "arXiv:astro-ph/9301001v1 7 Jan 1993", ); expect(response.body.data.markdown).not.toContain( - "h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm" + "h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm", ); const responseNoParsePDF: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) .set("Content-Type", "application/json") .send({ url: "https://arxiv.org/pdf/astro-ph/9301001.pdf", - parsePDF: false + parsePDF: false, }); await new Promise((r) => setTimeout(r, 6000)); @@ -376,10 +376,10 @@ describe("E2E Tests for v1 API Routes", () => { throw new Error("Expected response body to have 'data' property"); } expect(responseNoParsePDF.body.data.markdown).toContain( - "h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm" + "h7uKu14adDL6yGfnGf2qycY5uq8kC3OKCWkPxm", ); }, - 30000 + 30000, ); // it.concurrent("should handle 'location' parameter correctly", @@ -408,11 +408,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: "https://expired.badssl.com/", - timeout: 120000 + timeout: 120000, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -430,7 +430,7 @@ describe("E2E Tests for v1 API Routes", () => { const scrapeRequestWithSkipTlsVerification = { url: "https://expired.badssl.com/", skipTlsVerification: true, - timeout: 120000 + timeout: 120000, } as ScrapeRequest; const responseWithSkipTlsVerification: ScrapeResponseRequestTest = @@ -448,10 +448,10 @@ describe("E2E Tests for v1 API Routes", () => { } // console.log(responseWithSkipTlsVerification.body.data) expect(responseWithSkipTlsVerification.body.data.markdown).toContain( - "badssl.com" + "badssl.com", ); }, - 60000 + 60000, ); it.concurrent( @@ -459,11 +459,11 @@ describe("E2E Tests for v1 API Routes", () => { async () => { const scrapeRequest = { url: E2E_TEST_SERVER_URL, - removeBase64Images: true + removeBase64Images: true, } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -478,7 +478,7 @@ describe("E2E Tests for v1 API Routes", () => { // - TODO: not working for every image // expect(response.body.data.markdown).toContain("Image-Removed"); }, - 30000 + 30000, ); it.concurrent( @@ -489,13 +489,13 @@ describe("E2E Tests for v1 API Routes", () => { actions: [ { type: "wait", - milliseconds: 10000 - } - ] + milliseconds: 10000, + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -508,10 +508,10 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).not.toContain("Loading..."); expect(response.body.data.markdown).toContain( - "Content loaded after 5 seconds!" + "Content loaded after 5 seconds!", ); }, - 30000 + 30000, ); // screenshot @@ -522,13 +522,13 @@ describe("E2E Tests for v1 API Routes", () => { url: E2E_TEST_SERVER_URL, actions: [ { - type: "screenshot" - } - ] + type: "screenshot", + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -543,15 +543,15 @@ describe("E2E Tests for v1 API Routes", () => { throw new Error("Expected response body to have screenshots array"); } expect(response.body.data.actions.screenshots[0].length).toBeGreaterThan( - 0 + 0, ); expect(response.body.data.actions.screenshots[0]).toContain( - "https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-" + "https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-", ); // TODO compare screenshot with expected screenshot }, - 30000 + 30000, ); it.concurrent( @@ -562,16 +562,16 @@ describe("E2E Tests for v1 API Routes", () => { actions: [ { type: "screenshot", - fullPage: true + fullPage: true, }, { - type: "scrape" - } - ] + type: "scrape", + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -587,24 +587,24 @@ describe("E2E Tests for v1 API Routes", () => { throw new Error("Expected response body to have screenshots array"); } expect(response.body.data.actions.screenshots[0].length).toBeGreaterThan( - 0 + 0, ); expect(response.body.data.actions.screenshots[0]).toContain( - "https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-" + "https://service.firecrawl.dev/storage/v1/object/public/media/screenshot-", ); if (!response.body.data.actions?.scrapes) { throw new Error("Expected response body to have scrapes array"); } expect(response.body.data.actions.scrapes[0].url).toBe( - "https://firecrawl-e2e-test.vercel.app/" + "https://firecrawl-e2e-test.vercel.app/", ); expect(response.body.data.actions.scrapes[0].html).toContain( - "This page is used for end-to-end (e2e) testing with Firecrawl.

" + "This page is used for end-to-end (e2e) testing with Firecrawl.

", ); // TODO compare screenshot with expected full page screenshot }, - 30000 + 30000, ); it.concurrent( @@ -615,13 +615,13 @@ describe("E2E Tests for v1 API Routes", () => { actions: [ { type: "click", - selector: "#click-me" - } - ] + selector: "#click-me", + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -634,10 +634,10 @@ describe("E2E Tests for v1 API Routes", () => { } expect(response.body.data.markdown).not.toContain("Click me!"); expect(response.body.data.markdown).toContain( - "Text changed after click!" + "Text changed after click!", ); }, - 30000 + 30000, ); it.concurrent( @@ -649,17 +649,17 @@ describe("E2E Tests for v1 API Routes", () => { actions: [ { type: "click", - selector: "#input-1" + selector: "#input-1", }, { type: "write", - text: "Hello, world!" - } - ] + text: "Hello, world!", + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -675,7 +675,7 @@ describe("E2E Tests for v1 API Routes", () => { // uncomment the following line: // expect(response.body.data.html).toContain(""); }, - 30000 + 30000, ); // TODO: fix this test (need to fix fire-engine first) @@ -688,13 +688,13 @@ describe("E2E Tests for v1 API Routes", () => { actions: [ { type: "press", - key: "ArrowDown" - } - ] + key: "ArrowDown", + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -709,7 +709,7 @@ describe("E2E Tests for v1 API Routes", () => { // } // expect(response.body.data.markdown).toContain("Last Key Clicked: ArrowDown") }, - 30000 + 30000, ); // TODO: fix this test (need to fix fire-engine first) @@ -722,18 +722,18 @@ describe("E2E Tests for v1 API Routes", () => { actions: [ { type: "click", - selector: "#scroll-bottom-loader" + selector: "#scroll-bottom-loader", }, { type: "scroll", direction: "down", - amount: 2000 - } - ] + amount: 2000, + }, + ], } as ScrapeRequest; const response: ScrapeResponseRequestTest = await request( - FIRECRAWL_API_URL + FIRECRAWL_API_URL, ) .post("/v1/scrape") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`) @@ -748,7 +748,7 @@ describe("E2E Tests for v1 API Routes", () => { // // expect(response.body.data.markdown).toContain("You have reached the bottom!") }, - 30000 + 30000, ); // TODO: test scrape action diff --git a/apps/api/src/__tests__/e2e_withAuth/index.test.ts b/apps/api/src/__tests__/e2e_withAuth/index.test.ts index e026eef0..46668e64 100644 --- a/apps/api/src/__tests__/e2e_withAuth/index.test.ts +++ b/apps/api/src/__tests__/e2e_withAuth/index.test.ts @@ -3,7 +3,7 @@ import dotenv from "dotenv"; import { FirecrawlCrawlResponse, FirecrawlCrawlStatusResponse, - FirecrawlScrapeResponse + FirecrawlScrapeResponse, } from "../../types"; dotenv.config(); @@ -42,7 +42,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -63,30 +63,30 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data.metadata.pageError).toBeUndefined(); expect(response.body.data.metadata.title).toBe("Roast My Website"); expect(response.body.data.metadata.description).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(response.body.data.metadata.keywords).toBe( - "Roast My Website,Roast,Website,GitHub,Firecrawl" + "Roast My Website,Roast,Website,GitHub,Firecrawl", ); expect(response.body.data.metadata.robots).toBe("follow, index"); expect(response.body.data.metadata.ogTitle).toBe("Roast My Website"); expect(response.body.data.metadata.ogDescription).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(response.body.data.metadata.ogUrl).toBe( - "https://www.roastmywebsite.ai" + "https://www.roastmywebsite.ai", ); expect(response.body.data.metadata.ogImage).toBe( - "https://www.roastmywebsite.ai/og.png" + "https://www.roastmywebsite.ai/og.png", ); expect(response.body.data.metadata.ogLocaleAlternate).toStrictEqual([]); expect(response.body.data.metadata.ogSiteName).toBe("Roast My Website"); expect(response.body.data.metadata.sourceURL).toBe( - "https://roastmywebsite.ai" + "https://roastmywebsite.ai", ); expect(response.body.data.metadata.pageStatusCode).toBe(200); }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( @@ -98,7 +98,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://roastmywebsite.ai", - pageOptions: { includeHtml: true } + pageOptions: { includeHtml: true }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -112,7 +112,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( @@ -130,12 +130,12 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("content"); expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.content).toContain( - "We present spectrophotometric observations of the Broad Line Radio Galaxy" + "We present spectrophotometric observations of the Broad Line Radio Galaxy", ); expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -153,12 +153,12 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("content"); expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.content).toContain( - "We present spectrophotometric observations of the Broad Line Radio Galaxy" + "We present spectrophotometric observations of the Broad Line Radio Galaxy", ); expect(response.body.data.metadata.pageStatusCode).toBe(200); expect(response.body.data.metadata.pageError).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -177,16 +177,16 @@ describe("E2E Tests for v0 API Routes", () => { expect(responseWithoutRemoveTags.body.data).toHaveProperty("metadata"); expect(responseWithoutRemoveTags.body.data).not.toHaveProperty("html"); expect(responseWithoutRemoveTags.body.data.content).toContain( - "Scrape This Site" + "Scrape This Site", ); expect(responseWithoutRemoveTags.body.data.content).toContain( - "Lessons and Videos" + "Lessons and Videos", ); // #footer expect(responseWithoutRemoveTags.body.data.content).toContain( - "[Sandbox](" + "[Sandbox](", ); // .nav expect(responseWithoutRemoveTags.body.data.content).toContain( - "web scraping" + "web scraping", ); // strong const response: FirecrawlScrapeResponse = await request(TEST_URL) @@ -195,7 +195,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com/", - pageOptions: { removeTags: [".nav", "#footer", "strong"] } + pageOptions: { removeTags: [".nav", "#footer", "strong"] }, }); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("data"); @@ -208,7 +208,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data.content).not.toContain("[Sandbox]("); // .nav expect(response.body.data.content).not.toContain("web scraping"); // strong }, - 30000 + 30000, ); // 30 seconds timeout it.concurrent( @@ -227,10 +227,10 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(400); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "bad request" + "bad request", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -249,10 +249,10 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(401); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "unauthorized" + "unauthorized", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -271,10 +271,10 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(403); expect(response.body.data.metadata.pageError.toLowerCase()).toContain( - "forbidden" + "forbidden", ); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -293,7 +293,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(404); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -312,7 +312,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(405); }, - 60000 + 60000, ); // 60 seconds it.concurrent( @@ -331,7 +331,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.data).toHaveProperty("metadata"); expect(response.body.data.metadata.pageStatusCode).toBe(500); }, - 60000 + 60000, ); // 60 seconds }); @@ -351,7 +351,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -365,9 +365,9 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty("jobId"); expect(response.body.jobId).toMatch( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/, ); - } + }, ); it.concurrent( @@ -381,8 +381,8 @@ describe("E2E Tests for v0 API Routes", () => { url: "https://mendable.ai", limit: 10, crawlerOptions: { - includes: ["blog/*"] - } + includes: ["blog/*"], + }, }); let response: FirecrawlCrawlStatusResponse; @@ -408,7 +408,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(5); urls.forEach((url: string) => { @@ -424,13 +424,13 @@ describe("E2E Tests for v0 API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].content).toContain("Mendable"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); }, - 180000 + 180000, ); // 180 seconds it.concurrent( @@ -444,8 +444,8 @@ describe("E2E Tests for v0 API Routes", () => { url: "https://mendable.ai", limit: 10, crawlerOptions: { - excludes: ["blog/*"] - } + excludes: ["blog/*"], + }, }); let isFinished = false; @@ -467,20 +467,20 @@ describe("E2E Tests for v0 API Routes", () => { await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for data to be saved on the database const completedResponse: FirecrawlCrawlStatusResponse = await request( - TEST_URL + TEST_URL, ) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(5); urls.forEach((url: string) => { expect(url.startsWith("https://wwww.mendable.ai/blog/")).toBeFalsy(); }); }, - 90000 + 90000, ); // 90 seconds it.concurrent( @@ -492,7 +492,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://www.scrapethissite.com", - crawlerOptions: { maxDepth: 1 } + crawlerOptions: { maxDepth: 1 }, }); expect(crawlResponse.statusCode).toBe(200); @@ -515,7 +515,7 @@ describe("E2E Tests for v0 API Routes", () => { } } const completedResponse: FirecrawlCrawlStatusResponse = await request( - TEST_URL + TEST_URL, ) .get(`/v0/crawl/status/${crawlResponse.body.jobId}`) .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); @@ -528,13 +528,13 @@ describe("E2E Tests for v0 API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("markdown"); expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); const urls = completedResponse.body.data.map( - (item: any) => item.metadata?.sourceURL + (item: any) => item.metadata?.sourceURL, ); expect(urls.length).toBeGreaterThan(1); @@ -550,14 +550,14 @@ describe("E2E Tests for v0 API Routes", () => { expect(depth).toBeLessThanOrEqual(2); }); }, - 180000 + 180000, ); }); describe("POST /v0/crawlWebsitePreview", () => { it.concurrent("should require authorization", async () => { const response: FirecrawlCrawlResponse = await request(TEST_URL).post( - "/v0/crawlWebsitePreview" + "/v0/crawlWebsitePreview", ); expect(response.statusCode).toBe(401); }); @@ -571,7 +571,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://firecrawl.dev" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -585,7 +585,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.statusCode).toBe(408); }, - 3000 + 3000, ); }); @@ -604,7 +604,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ query: "test" }); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -620,7 +620,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(response.body.success).toBe(true); expect(response.body).toHaveProperty("data"); }, - 60000 + 60000, ); // 60 seconds timeout }); @@ -637,7 +637,7 @@ describe("E2E Tests for v0 API Routes", () => { .get("/v0/crawl/status/123") .set("Authorization", `Bearer invalid-api-key`); expect(response.statusCode).toBe(401); - } + }, ); it.concurrent( @@ -647,7 +647,7 @@ describe("E2E Tests for v0 API Routes", () => { .get("/v0/crawl/status/invalidJobId") .set("Authorization", `Bearer ${process.env.TEST_API_KEY}`); expect(response.statusCode).toBe(404); - } + }, ); it.concurrent( @@ -689,22 +689,22 @@ describe("E2E Tests for v0 API Routes", () => { expect(completedResponse.body.data[0]).toHaveProperty("metadata"); expect(completedResponse.body.data[0].content).toContain("Firecrawl"); expect(completedResponse.body.data[0].metadata.pageStatusCode).toBe( - 200 + 200, ); expect( - completedResponse.body.data[0].metadata.pageError + completedResponse.body.data[0].metadata.pageError, ).toBeUndefined(); const childrenLinks = completedResponse.body.data.filter( (doc) => doc.metadata && doc.metadata.sourceURL && - doc.metadata.sourceURL.includes("firecrawl.dev/blog") + doc.metadata.sourceURL.includes("firecrawl.dev/blog"), ); expect(childrenLinks.length).toBe(completedResponse.body.data.length); }, - 180000 + 180000, ); // 120 seconds // TODO: review the test below @@ -762,7 +762,7 @@ describe("E2E Tests for v0 API Routes", () => { .set("Content-Type", "application/json") .send({ url: "https://docs.tatum.io", - crawlerOptions: { limit: 200 } + crawlerOptions: { limit: 200 }, }); expect(crawlResponse.statusCode).toBe(200); @@ -798,22 +798,22 @@ describe("E2E Tests for v0 API Routes", () => { expect(completedResponse.body.data).toEqual(expect.arrayContaining([])); expect(completedResponse.body).toHaveProperty("partial_data"); expect(completedResponse.body.partial_data[0]).toHaveProperty( - "content" + "content", ); expect(completedResponse.body.partial_data[0]).toHaveProperty( - "markdown" + "markdown", ); expect(completedResponse.body.partial_data[0]).toHaveProperty( - "metadata" + "metadata", ); expect( - completedResponse.body.partial_data[0].metadata.pageStatusCode + completedResponse.body.partial_data[0].metadata.pageStatusCode, ).toBe(200); expect( - completedResponse.body.partial_data[0].metadata.pageError + completedResponse.body.partial_data[0].metadata.pageError, ).toBeUndefined(); }, - 60000 + 60000, ); // 60 seconds }); @@ -828,7 +828,7 @@ describe("E2E Tests for v0 API Routes", () => { .send({ url: "https://mendable.ai", pageOptions: { - onlyMainContent: true + onlyMainContent: true, }, extractorOptions: { mode: "llm-extraction", @@ -838,18 +838,18 @@ describe("E2E Tests for v0 API Routes", () => { type: "object", properties: { company_mission: { - type: "string" + type: "string", }, supports_sso: { - type: "boolean" + type: "boolean", }, is_open_source: { - type: "boolean" - } + type: "boolean", + }, }, - required: ["company_mission", "supports_sso", "is_open_source"] - } - } + required: ["company_mission", "supports_sso", "is_open_source"], + }, + }, }); // Ensure that the job was successfully created before proceeding with LLM extraction @@ -868,7 +868,7 @@ describe("E2E Tests for v0 API Routes", () => { expect(llmExtraction.is_open_source).toBe(false); expect(typeof llmExtraction.is_open_source).toBe("boolean"); }, - 60000 + 60000, ); // 60 secs }); }); diff --git a/apps/api/src/controllers/__tests__/crawl.test.ts b/apps/api/src/controllers/__tests__/crawl.test.ts index 81fa2e5d..a004ee3c 100644 --- a/apps/api/src/controllers/__tests__/crawl.test.ts +++ b/apps/api/src/controllers/__tests__/crawl.test.ts @@ -10,9 +10,9 @@ jest.mock("../auth", () => ({ success: true, team_id: "team123", error: null, - status: 200 + status: 200, }), - reduce: jest.fn() + reduce: jest.fn(), })); jest.mock("../../services/idempotency/validate"); @@ -21,15 +21,15 @@ describe("crawlController", () => { const req = { headers: { "x-idempotency-key": await uuidv4(), - Authorization: `Bearer ${process.env.TEST_API_KEY}` + Authorization: `Bearer ${process.env.TEST_API_KEY}`, }, body: { - url: "https://mendable.ai" - } + url: "https://mendable.ai", + }, } as unknown as Request; const res = { status: jest.fn().mockReturnThis(), - json: jest.fn() + json: jest.fn(), } as unknown as Response; // Mock the idempotency key validation to return false for the second call @@ -45,7 +45,7 @@ describe("crawlController", () => { await crawlController(req, res); expect(res.status).toHaveBeenCalledWith(409); expect(res.json).toHaveBeenCalledWith({ - error: "Idempotency key already used" + error: "Idempotency key already used", }); }); }); diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index 947c2784..f865984a 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -4,7 +4,7 @@ import { AuthResponse, NotificationType, PlanType, - RateLimiterMode + RateLimiterMode, } from "../types"; import { supabase_service } from "../services/supabase"; import { withAuth } from "../lib/withAuth"; @@ -41,7 +41,7 @@ export async function setCachedACUC( acuc: | AuthCreditUsageChunk | null - | ((acuc: AuthCreditUsageChunk) => AuthCreditUsageChunk | null) + | ((acuc: AuthCreditUsageChunk) => AuthCreditUsageChunk | null), ) { const cacheKeyACUC = `acuc_${api_key}`; const redLockKey = `lock_${cacheKeyACUC}`; @@ -76,7 +76,7 @@ export async function setCachedACUC( export async function getACUC( api_key: string, cacheOnly = false, - useCache = true + useCache = true, ): Promise { const cacheKeyACUC = `acuc_${api_key}`; @@ -97,7 +97,7 @@ export async function getACUC( ({ data, error } = await supabase_service.rpc( "auth_credit_usage_chunk_test_21_credit_pack", { input_key: api_key }, - { get: true } + { get: true }, )); if (!error) { @@ -105,13 +105,13 @@ export async function getACUC( } logger.warn( - `Failed to retrieve authentication and credit usage data after ${retries}, trying again...` + `Failed to retrieve authentication and credit usage data after ${retries}, trying again...`, ); retries++; if (retries === maxRetries) { throw new Error( "Failed to retrieve authentication and credit usage data after 3 attempts: " + - JSON.stringify(error) + JSON.stringify(error), ); } @@ -143,19 +143,19 @@ export async function clearACUC(api_key: string): Promise { export async function authenticateUser( req, res, - mode?: RateLimiterMode + mode?: RateLimiterMode, ): Promise { return withAuth(supaAuthenticateUser, { success: true, chunk: null, - team_id: "bypass" + team_id: "bypass", })(req, res, mode); } export async function supaAuthenticateUser( req, res, - mode?: RateLimiterMode + mode?: RateLimiterMode, ): Promise { const authHeader = req.headers.authorization ?? @@ -170,7 +170,7 @@ export async function supaAuthenticateUser( return { success: false, error: "Unauthorized: Token missing", - status: 401 + status: 401, }; } @@ -199,7 +199,7 @@ export async function supaAuthenticateUser( return { success: false, error: "Unauthorized: Invalid token", - status: 401 + status: 401, }; } @@ -209,7 +209,7 @@ export async function supaAuthenticateUser( return { success: false, error: "Unauthorized: Invalid token", - status: 401 + status: 401, }; } @@ -219,14 +219,14 @@ export async function supaAuthenticateUser( const plan = getPlanByPriceId(priceId); subscriptionData = { team_id: teamId, - plan + plan, }; switch (mode) { case RateLimiterMode.Crawl: rateLimiter = getRateLimiter( RateLimiterMode.Crawl, token, - subscriptionData.plan + subscriptionData.plan, ); break; case RateLimiterMode.Scrape: @@ -234,21 +234,21 @@ export async function supaAuthenticateUser( RateLimiterMode.Scrape, token, subscriptionData.plan, - teamId + teamId, ); break; case RateLimiterMode.Search: rateLimiter = getRateLimiter( RateLimiterMode.Search, token, - subscriptionData.plan + subscriptionData.plan, ); break; case RateLimiterMode.Map: rateLimiter = getRateLimiter( RateLimiterMode.Map, token, - subscriptionData.plan + subscriptionData.plan, ); break; case RateLimiterMode.CrawlStatus: @@ -278,7 +278,7 @@ export async function supaAuthenticateUser( priceId, plan: subscriptionData?.plan, mode, - rateLimiterRes + rateLimiterRes, }); const secs = Math.round(rateLimiterRes.msBeforeNext / 1000) || 1; const retryDate = new Date(Date.now() + rateLimiterRes.msBeforeNext); @@ -293,7 +293,7 @@ export async function supaAuthenticateUser( return { success: false, error: `Rate limit exceeded. Consumed (req/min): ${rateLimiterRes.consumedPoints}, Remaining (req/min): ${rateLimiterRes.remainingPoints}. Upgrade your plan at https://firecrawl.dev/pricing for increased rate limits or please retry after ${secs}s, resets at ${retryDate}`, - status: 429 + status: 429, }; } @@ -323,7 +323,7 @@ export async function supaAuthenticateUser( success: true, team_id: teamId ?? undefined, plan: (subscriptionData?.plan ?? "") as PlanType, - chunk + chunk, }; } function getPlanByPriceId(price_id: string | null): PlanType { diff --git a/apps/api/src/controllers/v0/admin/queue.ts b/apps/api/src/controllers/v0/admin/queue.ts index 6cc1c6e0..d7d9c089 100644 --- a/apps/api/src/controllers/v0/admin/queue.ts +++ b/apps/api/src/controllers/v0/admin/queue.ts @@ -8,7 +8,7 @@ import { sendSlackWebhook } from "../../../services/alerts/slack"; export async function cleanBefore24hCompleteJobsController( req: Request, - res: Response + res: Response, ) { logger.info("🐂 Cleaning jobs older than 24h"); try { @@ -22,8 +22,8 @@ export async function cleanBefore24hCompleteJobsController( ["completed"], i * batchSize, i * batchSize + batchSize, - true - ) + true, + ), ); } const completedJobs: Job[] = ( @@ -33,7 +33,7 @@ export async function cleanBefore24hCompleteJobsController( completedJobs.filter( (job) => job.finishedOn !== undefined && - job.finishedOn < Date.now() - 24 * 60 * 60 * 1000 + job.finishedOn < Date.now() - 24 * 60 * 60 * 1000, ) || []; let count = 0; @@ -73,14 +73,14 @@ export async function queuesController(req: Request, res: Response) { const scrapeQueue = getScrapeQueue(); const [webScraperActive] = await Promise.all([ - scrapeQueue.getActiveCount() + scrapeQueue.getActiveCount(), ]); const noActiveJobs = webScraperActive === 0; // 200 if no active jobs, 503 if there are active jobs return res.status(noActiveJobs ? 200 : 500).json({ webScraperActive, - noActiveJobs + noActiveJobs, }); } catch (error) { logger.error(error); @@ -99,7 +99,7 @@ export async function autoscalerController(req: Request, res: Response) { await Promise.all([ scrapeQueue.getActiveCount(), scrapeQueue.getWaitingCount(), - scrapeQueue.getPrioritizedCount() + scrapeQueue.getPrioritizedCount(), ]); let waitingAndPriorityCount = webScraperWaiting + webScraperPriority; @@ -109,9 +109,9 @@ export async function autoscalerController(req: Request, res: Response) { "https://api.machines.dev/v1/apps/firecrawl-scraper-js/machines", { headers: { - Authorization: `Bearer ${process.env.FLY_API_TOKEN}` - } - } + Authorization: `Bearer ${process.env.FLY_API_TOKEN}`, + }, + }, ); const machines = await request.json(); @@ -121,7 +121,7 @@ export async function autoscalerController(req: Request, res: Response) { (machine.state === "started" || machine.state === "starting" || machine.state === "replacing") && - machine.config.env["FLY_PROCESS_GROUP"] === "worker" + machine.config.env["FLY_PROCESS_GROUP"] === "worker", ).length; let targetMachineCount = activeMachines; @@ -134,17 +134,17 @@ export async function autoscalerController(req: Request, res: Response) { if (webScraperActive > 9000 || waitingAndPriorityCount > 2000) { targetMachineCount = Math.min( maxNumberOfMachines, - activeMachines + baseScaleUp * 3 + activeMachines + baseScaleUp * 3, ); } else if (webScraperActive > 5000 || waitingAndPriorityCount > 1000) { targetMachineCount = Math.min( maxNumberOfMachines, - activeMachines + baseScaleUp * 2 + activeMachines + baseScaleUp * 2, ); } else if (webScraperActive > 1000 || waitingAndPriorityCount > 500) { targetMachineCount = Math.min( maxNumberOfMachines, - activeMachines + baseScaleUp + activeMachines + baseScaleUp, ); } @@ -152,47 +152,47 @@ export async function autoscalerController(req: Request, res: Response) { if (webScraperActive < 100 && waitingAndPriorityCount < 50) { targetMachineCount = Math.max( minNumberOfMachines, - activeMachines - baseScaleDown * 3 + activeMachines - baseScaleDown * 3, ); } else if (webScraperActive < 500 && waitingAndPriorityCount < 200) { targetMachineCount = Math.max( minNumberOfMachines, - activeMachines - baseScaleDown * 2 + activeMachines - baseScaleDown * 2, ); } else if (webScraperActive < 1000 && waitingAndPriorityCount < 500) { targetMachineCount = Math.max( minNumberOfMachines, - activeMachines - baseScaleDown + activeMachines - baseScaleDown, ); } if (targetMachineCount !== activeMachines) { logger.info( - `🐂 Scaling from ${activeMachines} to ${targetMachineCount} - ${webScraperActive} active, ${webScraperWaiting} waiting` + `🐂 Scaling from ${activeMachines} to ${targetMachineCount} - ${webScraperActive} active, ${webScraperWaiting} waiting`, ); if (targetMachineCount > activeMachines) { sendSlackWebhook( `🐂 Scaling from ${activeMachines} to ${targetMachineCount} - ${webScraperActive} active, ${webScraperWaiting} waiting - Current DateTime: ${new Date().toISOString()}`, false, - process.env.SLACK_AUTOSCALER ?? "" + process.env.SLACK_AUTOSCALER ?? "", ); } else { sendSlackWebhook( `🐂 Scaling from ${activeMachines} to ${targetMachineCount} - ${webScraperActive} active, ${webScraperWaiting} waiting - Current DateTime: ${new Date().toISOString()}`, false, - process.env.SLACK_AUTOSCALER ?? "" + process.env.SLACK_AUTOSCALER ?? "", ); } return res.status(200).json({ mode: "scale-descale", - count: targetMachineCount + count: targetMachineCount, }); } return res.status(200).json({ mode: "normal", - count: activeMachines + count: activeMachines, }); } catch (error) { logger.error(error); diff --git a/apps/api/src/controllers/v0/admin/redis-health.ts b/apps/api/src/controllers/v0/admin/redis-health.ts index 963755ef..b3256edf 100644 --- a/apps/api/src/controllers/v0/admin/redis-health.ts +++ b/apps/api/src/controllers/v0/admin/redis-health.ts @@ -38,7 +38,7 @@ export async function redisHealthController(req: Request, res: Response) { try { await retryOperation(() => redisRateLimitClient.set(testKey, testValue)); redisRateLimitHealth = await retryOperation(() => - redisRateLimitClient.get(testKey) + redisRateLimitClient.get(testKey), ); await retryOperation(() => redisRateLimitClient.del(testKey)); } catch (error) { @@ -49,7 +49,7 @@ export async function redisHealthController(req: Request, res: Response) { const healthStatus = { queueRedis: queueRedisHealth === testValue ? "healthy" : "unhealthy", redisRateLimitClient: - redisRateLimitHealth === testValue ? "healthy" : "unhealthy" + redisRateLimitHealth === testValue ? "healthy" : "unhealthy", }; if ( @@ -60,7 +60,7 @@ export async function redisHealthController(req: Request, res: Response) { return res.status(200).json({ status: "healthy", details: healthStatus }); } else { logger.info( - `Redis instances health check: ${JSON.stringify(healthStatus)}` + `Redis instances health check: ${JSON.stringify(healthStatus)}`, ); // await sendSlackWebhook( // `[REDIS DOWN] Redis instances health check: ${JSON.stringify( diff --git a/apps/api/src/controllers/v0/crawl-cancel.ts b/apps/api/src/controllers/v0/crawl-cancel.ts index b445978c..db834230 100644 --- a/apps/api/src/controllers/v0/crawl-cancel.ts +++ b/apps/api/src/controllers/v0/crawl-cancel.ts @@ -48,7 +48,7 @@ export async function crawlCancelController(req: Request, res: Response) { } res.json({ - status: "cancelled" + status: "cancelled", }); } catch (error) { Sentry.captureException(error); diff --git a/apps/api/src/controllers/v0/crawl-status.ts b/apps/api/src/controllers/v0/crawl-status.ts index 756fca44..60ca0e7f 100644 --- a/apps/api/src/controllers/v0/crawl-status.ts +++ b/apps/api/src/controllers/v0/crawl-status.ts @@ -60,12 +60,12 @@ export async function crawlStatusController(req: Request, res: Response) { // Combine jobs and jobStatuses into a single array of objects let jobsWithStatuses = jobs.map((job, index) => ({ job, - status: jobStatuses[index] + status: jobStatuses[index], })); // Filter out failed jobs jobsWithStatuses = jobsWithStatuses.filter( - (x) => x.status !== "failed" && x.status !== "unknown" + (x) => x.status !== "failed" && x.status !== "unknown", ); // Sort jobs by timestamp @@ -84,10 +84,10 @@ export async function crawlStatusController(req: Request, res: Response) { const data = jobs .filter( (x) => - x.failedReason !== "Concurreny limit hit" && x.returnvalue !== null + x.failedReason !== "Concurreny limit hit" && x.returnvalue !== null, ) .map((x) => - Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue + Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue, ); if ( @@ -117,7 +117,7 @@ export async function crawlStatusController(req: Request, res: Response) { ? [] : data .filter((x) => x !== null) - .map((x) => toLegacyDocument(x, sc.internalOptions)) + .map((x) => toLegacyDocument(x, sc.internalOptions)), }); } catch (error) { Sentry.captureException(error); diff --git a/apps/api/src/controllers/v0/crawl.ts b/apps/api/src/controllers/v0/crawl.ts index bb9ba363..36b8309f 100644 --- a/apps/api/src/controllers/v0/crawl.ts +++ b/apps/api/src/controllers/v0/crawl.ts @@ -10,7 +10,7 @@ import { createIdempotencyKey } from "../../../src/services/idempotency/create"; import { defaultCrawlPageOptions, defaultCrawlerOptions, - defaultOrigin + defaultOrigin, } from "../../../src/lib/default-values"; import { v4 as uuidv4 } from "uuid"; import { logger } from "../../../src/lib/logger"; @@ -21,7 +21,7 @@ import { lockURL, lockURLs, saveCrawl, - StoredCrawl + StoredCrawl, } from "../../../src/lib/crawl-redis"; import { getScrapeQueue } from "../../../src/services/queue-service"; import { checkAndUpdateURL } from "../../../src/lib/validateUrl"; @@ -54,7 +54,7 @@ export async function crawlController(req: Request, res: Response) { const crawlerOptions = { ...defaultCrawlerOptions, - ...req.body.crawlerOptions + ...req.body.crawlerOptions, }; const pageOptions = { ...defaultCrawlPageOptions, ...req.body.pageOptions }; @@ -82,13 +82,13 @@ export async function crawlController(req: Request, res: Response) { const { success: creditsCheckSuccess, message: creditsCheckMessage, - remainingCredits + remainingCredits, } = await checkTeamCredits(chunk, team_id, limitCheck); if (!creditsCheckSuccess) { return res.status(402).json({ error: - "Insufficient credits. You may be requesting with a higher limit than the amount of credits you have left. If not, upgrade your plan at https://firecrawl.dev/pricing or contact us at help@firecrawl.com" + "Insufficient credits. You may be requesting with a higher limit than the amount of credits you have left. If not, upgrade your plan at https://firecrawl.dev/pricing or contact us at help@firecrawl.com", }); } @@ -113,7 +113,7 @@ export async function crawlController(req: Request, res: Response) { if (isUrlBlocked(url)) { return res.status(403).json({ error: - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", }); } @@ -153,7 +153,7 @@ export async function crawlController(req: Request, res: Response) { const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions( pageOptions, undefined, - undefined + undefined, ); internalOptions.disableSmartWaitCache = true; // NOTE: smart wait disabled for crawls to ensure contentful scrape, speed does not matter @@ -166,7 +166,7 @@ export async function crawlController(req: Request, res: Response) { internalOptions, team_id, plan, - createdAt: Date.now() + createdAt: Date.now(), }; const crawler = crawlToCrawler(id, sc); @@ -204,23 +204,23 @@ export async function crawlController(req: Request, res: Response) { plan, origin: req.body.origin ?? defaultOrigin, crawl_id: id, - sitemapped: true + sitemapped: true, }, opts: { jobId: uuid, - priority: jobPriority - } + priority: jobPriority, + }, }; }); await lockURLs( id, sc, - jobs.map((x) => x.data.url) + jobs.map((x) => x.data.url), ); await addCrawlJobs( id, - jobs.map((x) => x.opts.jobId) + jobs.map((x) => x.opts.jobId), ); for (const job of jobs) { // add with sentry instrumentation @@ -243,12 +243,12 @@ export async function crawlController(req: Request, res: Response) { team_id, plan: plan!, origin: req.body.origin ?? defaultOrigin, - crawl_id: id + crawl_id: id, }, { - priority: 15 // prioritize request 0 of crawl jobs same as scrape jobs + priority: 15, // prioritize request 0 of crawl jobs same as scrape jobs }, - jobId + jobId, ); await addCrawlJob(id, jobId); } @@ -258,7 +258,7 @@ export async function crawlController(req: Request, res: Response) { Sentry.captureException(error); logger.error(error); return res.status(500).json({ - error: error instanceof ZodError ? "Invalid URL" : error.message + error: error instanceof ZodError ? "Invalid URL" : error.message, }); } } diff --git a/apps/api/src/controllers/v0/crawlPreview.ts b/apps/api/src/controllers/v0/crawlPreview.ts index 3b47bfaa..405e49c2 100644 --- a/apps/api/src/controllers/v0/crawlPreview.ts +++ b/apps/api/src/controllers/v0/crawlPreview.ts @@ -9,7 +9,7 @@ import { crawlToCrawler, lockURL, saveCrawl, - StoredCrawl + StoredCrawl, } from "../../../src/lib/crawl-redis"; import { addScrapeJob } from "../../../src/services/queue-jobs"; import { checkAndUpdateURL } from "../../../src/lib/validateUrl"; @@ -43,7 +43,7 @@ export async function crawlPreviewController(req: Request, res: Response) { if (isUrlBlocked(url)) { return res.status(403).json({ error: - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", }); } @@ -51,7 +51,7 @@ export async function crawlPreviewController(req: Request, res: Response) { const pageOptions = req.body.pageOptions ?? { onlyMainContent: false, includeHtml: false, - removeTags: [] + removeTags: [], }; // if (mode === "single_urls" && !url.includes(",")) { // NOTE: do we need this? @@ -94,7 +94,7 @@ export async function crawlPreviewController(req: Request, res: Response) { const { scrapeOptions, internalOptions } = fromLegacyScrapeOptions( pageOptions, undefined, - undefined + undefined, ); const sc: StoredCrawl = { @@ -105,7 +105,7 @@ export async function crawlPreviewController(req: Request, res: Response) { team_id, plan, robots, - createdAt: Date.now() + createdAt: Date.now(), }; await saveCrawl(id, sc); @@ -131,10 +131,10 @@ export async function crawlPreviewController(req: Request, res: Response) { internalOptions, origin: "website-preview", crawl_id: id, - sitemapped: true + sitemapped: true, }, {}, - jobId + jobId, ); await addCrawlJob(id, jobId); } @@ -151,10 +151,10 @@ export async function crawlPreviewController(req: Request, res: Response) { scrapeOptions, internalOptions, origin: "website-preview", - crawl_id: id + crawl_id: id, }, {}, - jobId + jobId, ); await addCrawlJob(id, jobId); } diff --git a/apps/api/src/controllers/v0/scrape.ts b/apps/api/src/controllers/v0/scrape.ts index 4a761ea3..8501e502 100644 --- a/apps/api/src/controllers/v0/scrape.ts +++ b/apps/api/src/controllers/v0/scrape.ts @@ -2,7 +2,7 @@ import { ExtractorOptions, PageOptions } from "./../../lib/entities"; import { Request, Response } from "express"; import { billTeam, - checkTeamCredits + checkTeamCredits, } from "../../services/billing/credit_billing"; import { authenticateUser } from "../auth"; import { PlanType, RateLimiterMode } from "../../types"; @@ -11,7 +11,7 @@ import { Document, fromLegacyCombo, toLegacyDocument, - url as urlSchema + url as urlSchema, } from "../v1/types"; import { isUrlBlocked } from "../../scraper/WebScraper/utils/blocklist"; // Import the isUrlBlocked function import { numTokensFromString } from "../../lib/LLM-extraction/helpers"; @@ -19,7 +19,7 @@ import { defaultPageOptions, defaultExtractorOptions, defaultTimeout, - defaultOrigin + defaultOrigin, } from "../../lib/default-values"; import { addScrapeJob, waitForJob } from "../../services/queue-jobs"; import { getScrapeQueue } from "../../services/queue-service"; @@ -38,7 +38,7 @@ export async function scrapeHelper( pageOptions: PageOptions, extractorOptions: ExtractorOptions, timeout: number, - plan?: PlanType + plan?: PlanType, ): Promise<{ success: boolean; error?: string; @@ -55,7 +55,7 @@ export async function scrapeHelper( success: false, error: "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", - returnCode: 403 + returnCode: 403, }; } @@ -65,7 +65,7 @@ export async function scrapeHelper( pageOptions, extractorOptions, timeout, - crawlerOptions + crawlerOptions, ); await addScrapeJob( @@ -77,11 +77,11 @@ export async function scrapeHelper( internalOptions, plan: plan!, origin: req.body.origin ?? defaultOrigin, - is_scrape: true + is_scrape: true, }, {}, jobId, - jobPriority + jobPriority, ); let doc; @@ -90,7 +90,7 @@ export async function scrapeHelper( { name: "Wait for job to finish", op: "bullmq.wait", - attributes: { job: jobId } + attributes: { job: jobId }, }, async (span) => { try { @@ -104,20 +104,20 @@ export async function scrapeHelper( return { success: false, error: "Request timed out", - returnCode: 408 + returnCode: 408, }; } else if ( typeof e === "string" && (e.includes("Error generating completions: ") || e.includes("Invalid schema for function") || e.includes( - "LLM extraction did not match the extraction schema you provided." + "LLM extraction did not match the extraction schema you provided.", )) ) { return { success: false, error: e, - returnCode: 500 + returnCode: 500, }; } else { throw e; @@ -125,7 +125,7 @@ export async function scrapeHelper( } span.setAttribute("result", JSON.stringify(doc)); return null; - } + }, ); if (err !== null) { @@ -140,7 +140,7 @@ export async function scrapeHelper( success: true, error: "No page found", returnCode: 200, - data: doc + data: doc, }; } @@ -166,7 +166,7 @@ export async function scrapeHelper( return { success: true, data: toLegacyDocument(doc, internalOptions), - returnCode: 200 + returnCode: 200, }; } @@ -185,7 +185,7 @@ export async function scrapeController(req: Request, res: Response) { const pageOptions = { ...defaultPageOptions, ...req.body.pageOptions }; const extractorOptions = { ...defaultExtractorOptions, - ...req.body.extractorOptions + ...req.body.extractorOptions, }; const origin = req.body.origin ?? defaultOrigin; let timeout = req.body.timeout ?? defaultTimeout; @@ -197,7 +197,7 @@ export async function scrapeController(req: Request, res: Response) { ) { return res.status(400).json({ error: - "extractorOptions.extractionSchema must be an object if llm-extraction mode is specified" + "extractorOptions.extractionSchema must be an object if llm-extraction mode is specified", }); } @@ -213,7 +213,7 @@ export async function scrapeController(req: Request, res: Response) { earlyReturn = true; return res.status(402).json({ error: - "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing" + "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing", }); } } catch (error) { @@ -221,7 +221,7 @@ export async function scrapeController(req: Request, res: Response) { earlyReturn = true; return res.status(500).json({ error: - "Error checking team credits. Please contact help@firecrawl.com for help." + "Error checking team credits. Please contact help@firecrawl.com for help.", }); } @@ -236,7 +236,7 @@ export async function scrapeController(req: Request, res: Response) { pageOptions, extractorOptions, timeout, - plan + plan, ); const endTime = new Date().getTime(); const timeTakenInSeconds = (endTime - startTime) / 1000; @@ -244,7 +244,7 @@ export async function scrapeController(req: Request, res: Response) { result.data && (result.data as Document).markdown ? numTokensFromString( (result.data as Document).markdown!, - "gpt-3.5-turbo" + "gpt-3.5-turbo", ) : 0; @@ -267,7 +267,7 @@ export async function scrapeController(req: Request, res: Response) { // billing for doc done on queue end, bill only for llm extraction billTeam(team_id, chunk?.sub_id, creditsToBeBilled).catch((error) => { logger.error( - `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}` + `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`, ); // Optionally, you could notify an admin or add to a retry queue here }); @@ -290,7 +290,7 @@ export async function scrapeController(req: Request, res: Response) { const { scrapeOptions } = fromLegacyScrapeOptions( pageOptions, extractorOptions, - timeout + timeout, ); logJob({ @@ -306,7 +306,7 @@ export async function scrapeController(req: Request, res: Response) { crawlerOptions: crawlerOptions, scrapeOptions, origin: origin, - num_tokens: numTokens + num_tokens: numTokens, }); return res.status(result.returnCode).json(result); @@ -319,7 +319,7 @@ export async function scrapeController(req: Request, res: Response) { ? "Invalid URL" : typeof error === "string" ? error - : (error?.message ?? "Internal Server Error") + : (error?.message ?? "Internal Server Error"), }); } } diff --git a/apps/api/src/controllers/v0/search.ts b/apps/api/src/controllers/v0/search.ts index 4950ea5f..6a3513df 100644 --- a/apps/api/src/controllers/v0/search.ts +++ b/apps/api/src/controllers/v0/search.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { billTeam, - checkTeamCredits + checkTeamCredits, } from "../../services/billing/credit_billing"; import { authenticateUser } from "../auth"; import { PlanType, RateLimiterMode } from "../../types"; @@ -20,7 +20,7 @@ import { Document, fromLegacyCombo, fromLegacyScrapeOptions, - toLegacyDocument + toLegacyDocument, } from "../v1/types"; export async function searchHelper( @@ -31,7 +31,7 @@ export async function searchHelper( crawlerOptions: any, pageOptions: PageOptions, searchOptions: SearchOptions, - plan: PlanType | undefined + plan: PlanType | undefined, ): Promise<{ success: boolean; error?: string; @@ -62,7 +62,7 @@ export async function searchHelper( filter: filter, lang: searchOptions.lang ?? "en", country: searchOptions.country ?? "us", - location: searchOptions.location + location: searchOptions.location, }); let justSearch = pageOptions.fetchPageContent === false; @@ -71,13 +71,13 @@ export async function searchHelper( pageOptions, undefined, 60000, - crawlerOptions + crawlerOptions, ); if (justSearch) { billTeam(team_id, subscription_id, res.length).catch((error) => { logger.error( - `Failed to bill team ${team_id} for ${res.length} credits: ${error}` + `Failed to bill team ${team_id} for ${res.length} credits: ${error}`, ); // Optionally, you could notify an admin or add to a retry queue here }); @@ -107,12 +107,12 @@ export async function searchHelper( mode: "single_urls", team_id: team_id, scrapeOptions, - internalOptions + internalOptions, }, opts: { jobId: uuid, - priority: jobPriority - } + priority: jobPriority, + }, }; }); @@ -123,7 +123,7 @@ export async function searchHelper( const docs = ( await Promise.all( - jobDatas.map((x) => waitForJob(x.opts.jobId, 60000)) + jobDatas.map((x) => waitForJob(x.opts.jobId, 60000)), ) ).map((x) => toLegacyDocument(x, internalOptions)); @@ -136,7 +136,7 @@ export async function searchHelper( // make sure doc.content is not empty const filteredDocs = docs.filter( - (doc: any) => doc && doc.content && doc.content.trim().length > 0 + (doc: any) => doc && doc.content && doc.content.trim().length > 0, ); if (filteredDocs.length === 0) { @@ -144,14 +144,14 @@ export async function searchHelper( success: true, error: "No page found", returnCode: 200, - data: docs + data: docs, }; } return { success: true, data: filteredDocs, - returnCode: 200 + returnCode: 200, }; } @@ -169,7 +169,7 @@ export async function searchController(req: Request, res: Response) { onlyMainContent: req.body.pageOptions?.onlyMainContent ?? false, fetchPageContent: req.body.pageOptions?.fetchPageContent ?? true, removeTags: req.body.pageOptions?.removeTags ?? [], - fallback: req.body.pageOptions?.fallback ?? false + fallback: req.body.pageOptions?.fallback ?? false, }; const origin = req.body.origin ?? "api"; @@ -197,7 +197,7 @@ export async function searchController(req: Request, res: Response) { crawlerOptions, pageOptions, searchOptions, - plan + plan, ); const endTime = new Date().getTime(); const timeTakenInSeconds = (endTime - startTime) / 1000; @@ -212,7 +212,7 @@ export async function searchController(req: Request, res: Response) { mode: "search", url: req.body.query, crawlerOptions: crawlerOptions, - origin: origin + origin: origin, }); return res.status(result.returnCode).json(result); } catch (error) { diff --git a/apps/api/src/controllers/v0/status.ts b/apps/api/src/controllers/v0/status.ts index 73bfa159..c68579ea 100644 --- a/apps/api/src/controllers/v0/status.ts +++ b/apps/api/src/controllers/v0/status.ts @@ -6,7 +6,7 @@ import * as Sentry from "@sentry/node"; export async function crawlJobStatusPreviewController( req: Request, - res: Response + res: Response, ) { try { const sc = await getCrawl(req.params.jobId); @@ -26,7 +26,7 @@ export async function crawlJobStatusPreviewController( // } const jobs = (await getJobs(req.params.jobId, jobIDs)).sort( - (a, b) => a.timestamp - b.timestamp + (a, b) => a.timestamp - b.timestamp, ); const jobStatuses = await Promise.all(jobs.map((x) => x.getState())); const jobStatus = sc.cancelled @@ -38,7 +38,7 @@ export async function crawlJobStatusPreviewController( : "active"; const data = jobs.map((x) => - Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue + Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue, ); res.json({ @@ -48,7 +48,7 @@ export async function crawlJobStatusPreviewController( total: jobs.length, data: jobStatus === "completed" ? data : null, partial_data: - jobStatus === "completed" ? [] : data.filter((x) => x !== null) + jobStatus === "completed" ? [] : data.filter((x) => x !== null), }); } catch (error) { Sentry.captureException(error); diff --git a/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts b/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts index 1ce058a0..b455e5ab 100644 --- a/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts +++ b/apps/api/src/controllers/v1/__tests__/urlValidation.test.ts @@ -25,13 +25,13 @@ describe("URL Schema Validation", () => { it("should reject URLs without a valid top-level domain", () => { expect(() => url.parse("http://example")).toThrow( - "URL must have a valid top-level domain or be a valid path" + "URL must have a valid top-level domain or be a valid path", ); }); it("should reject blocked URLs", () => { expect(() => url.parse("https://facebook.com")).toThrow( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); @@ -47,28 +47,28 @@ describe("URL Schema Validation", () => { it("should handle URLs with subdomains that are blocked", () => { expect(() => url.parse("https://sub.facebook.com")).toThrow( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); it("should handle URLs with paths that are blocked", () => { expect(() => url.parse("http://facebook.com/path")).toThrow( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); expect(() => url.parse("https://facebook.com/another/path")).toThrow( - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", ); }); it("should reject malformed URLs starting with 'http://http'", () => { expect(() => url.parse("http://http://example.com")).toThrow( - "Invalid URL. Invalid protocol." + "Invalid URL. Invalid protocol.", ); }); it("should reject malformed URLs containing multiple 'http://'", () => { expect(() => - url.parse("http://example.com/http://example.com") + url.parse("http://example.com/http://example.com"), ).not.toThrow(); }); diff --git a/apps/api/src/controllers/v1/batch-scrape.ts b/apps/api/src/controllers/v1/batch-scrape.ts index a78264e3..89fa6741 100644 --- a/apps/api/src/controllers/v1/batch-scrape.ts +++ b/apps/api/src/controllers/v1/batch-scrape.ts @@ -5,14 +5,14 @@ import { batchScrapeRequestSchema, CrawlResponse, RequestWithAuth, - ScrapeOptions + ScrapeOptions, } from "./types"; import { addCrawlJobs, getCrawl, lockURLs, saveCrawl, - StoredCrawl + StoredCrawl, } from "../../lib/crawl-redis"; import { logCrawl } from "../../services/logging/crawl_log"; import { getJobPriority } from "../../lib/job-priority"; @@ -22,7 +22,7 @@ import { logger as _logger } from "../../lib/logger"; export async function batchScrapeController( req: RequestWithAuth<{}, CrawlResponse, BatchScrapeRequest>, - res: Response + res: Response, ) { req.body = batchScrapeRequestSchema.parse(req.body); @@ -33,12 +33,12 @@ export async function batchScrapeController( module: "api/v1", method: "batchScrapeController", teamId: req.auth.team_id, - plan: req.auth.plan + plan: req.auth.plan, }); logger.debug("Batch scrape " + id + " starting", { urlsLength: req.body.urls, appendToId: req.body.appendToId, - account: req.account + account: req.account, }); if (!req.body.appendToId) { @@ -59,7 +59,7 @@ export async function batchScrapeController( internalOptions: { disableSmartWaitCache: true }, // NOTE: smart wait disabled for batch scrapes to ensure contentful scrape, speed does not matter team_id: req.auth.team_id, createdAt: Date.now(), - plan: req.auth.plan + plan: req.auth.plan, }; if (!req.body.appendToId) { @@ -75,7 +75,7 @@ export async function batchScrapeController( jobPriority = await getJobPriority({ plan: req.auth.plan, team_id: req.auth.team_id, - basePriority: 21 + basePriority: 21, }); } logger.debug("Using job priority " + jobPriority, { jobPriority }); @@ -97,12 +97,12 @@ export async function batchScrapeController( crawl_id: id, sitemapped: true, v1: true, - webhook: req.body.webhook + webhook: req.body.webhook, }, opts: { jobId: uuidv4(), - priority: 20 - } + priority: 20, + }, }; }); @@ -110,19 +110,19 @@ export async function batchScrapeController( await lockURLs( id, sc, - jobs.map((x) => x.data.url) + jobs.map((x) => x.data.url), ); logger.debug("Adding scrape jobs to Redis..."); await addCrawlJobs( id, - jobs.map((x) => x.opts.jobId) + jobs.map((x) => x.opts.jobId), ); logger.debug("Adding scrape jobs to BullMQ..."); await addScrapeJobs(jobs); if (req.body.webhook) { logger.debug("Calling webhook with batch_scrape.started...", { - webhook: req.body.webhook + webhook: req.body.webhook, }); await callWebhook( req.auth.team_id, @@ -130,7 +130,7 @@ export async function batchScrapeController( null, req.body.webhook, true, - "batch_scrape.started" + "batch_scrape.started", ); } @@ -139,6 +139,6 @@ export async function batchScrapeController( return res.status(200).json({ success: true, id, - url: `${protocol}://${req.get("host")}/v1/batch/scrape/${id}` + url: `${protocol}://${req.get("host")}/v1/batch/scrape/${id}`, }); } diff --git a/apps/api/src/controllers/v1/concurrency-check.ts b/apps/api/src/controllers/v1/concurrency-check.ts index bd25c73b..5ed569f5 100644 --- a/apps/api/src/controllers/v1/concurrency-check.ts +++ b/apps/api/src/controllers/v1/concurrency-check.ts @@ -2,7 +2,7 @@ import { authenticateUser } from "../auth"; import { ConcurrencyCheckParams, ConcurrencyCheckResponse, - RequestWithAuth + RequestWithAuth, } from "./types"; import { RateLimiterMode } from "../../types"; import { Response } from "express"; @@ -10,14 +10,14 @@ import { redisConnection } from "../../services/queue-service"; // Basically just middleware and error wrapping export async function concurrencyCheckController( req: RequestWithAuth, - res: Response + res: Response, ) { const concurrencyLimiterKey = "concurrency-limiter:" + req.auth.team_id; const now = Date.now(); const activeJobsOfTeam = await redisConnection.zrangebyscore( concurrencyLimiterKey, now, - Infinity + Infinity, ); return res .status(200) diff --git a/apps/api/src/controllers/v1/crawl-cancel.ts b/apps/api/src/controllers/v1/crawl-cancel.ts index 986ff104..00af8b31 100644 --- a/apps/api/src/controllers/v1/crawl-cancel.ts +++ b/apps/api/src/controllers/v1/crawl-cancel.ts @@ -9,7 +9,7 @@ configDotenv(); export async function crawlCancelController( req: RequestWithAuth<{ jobId: string }>, - res: Response + res: Response, ) { try { const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; @@ -43,7 +43,7 @@ export async function crawlCancelController( } res.json({ - status: "cancelled" + status: "cancelled", }); } catch (error) { Sentry.captureException(error); diff --git a/apps/api/src/controllers/v1/crawl-status-ws.ts b/apps/api/src/controllers/v1/crawl-status-ws.ts index d9994d97..817dc184 100644 --- a/apps/api/src/controllers/v1/crawl-status-ws.ts +++ b/apps/api/src/controllers/v1/crawl-status-ws.ts @@ -6,7 +6,7 @@ import { CrawlStatusResponse, Document, ErrorResponse, - RequestWithAuth + RequestWithAuth, } from "./types"; import { WebSocket } from "ws"; import { v4 as uuidv4 } from "uuid"; @@ -19,7 +19,7 @@ import { getDoneJobsOrderedLength, getThrottledJobs, isCrawlFinished, - isCrawlFinishedLocked + isCrawlFinishedLocked, } from "../../lib/crawl-redis"; import { getScrapeQueue } from "../../services/queue-service"; import { getJob, getJobs } from "./crawl-status"; @@ -64,7 +64,7 @@ function close(ws: WebSocket, code: number, msg: Message) { async function crawlStatusWS( ws: WebSocket, - req: RequestWithAuth + req: RequestWithAuth, ) { const sc = await getCrawl(req.params.jobId); if (!sc) { @@ -89,7 +89,10 @@ async function crawlStatusWS( const notDoneJobIDs = jobIDs.filter((x) => !doneJobIDs.includes(x)); const jobStatuses = await Promise.all( - notDoneJobIDs.map(async (x) => [x, await getScrapeQueue().getJobState(x)]) + notDoneJobIDs.map(async (x) => [ + x, + await getScrapeQueue().getJobState(x), + ]), ); const newlyDoneJobIDs: string[] = jobStatuses .filter((x) => x[1] === "completed" || x[1] === "failed") @@ -102,7 +105,7 @@ async function crawlStatusWS( if (job.returnvalue) { send(ws, { type: "document", - data: job.returnvalue + data: job.returnvalue, }); } else { return close(ws, 3000, { type: "error", error: job.failedReason }); @@ -120,7 +123,9 @@ async function crawlStatusWS( let jobIDs = await getCrawlJobs(req.params.jobId); let jobStatuses = await Promise.all( - jobIDs.map(async (x) => [x, await getScrapeQueue().getJobState(x)] as const) + jobIDs.map( + async (x) => [x, await getScrapeQueue().getJobState(x)] as const, + ), ); const throttledJobs = new Set(...(await getThrottledJobs(req.auth.team_id))); @@ -161,8 +166,8 @@ async function crawlStatusWS( completed: doneJobIDs.length, creditsUsed: jobIDs.length, expiresAt: (await getCrawlExpiry(req.params.jobId)).toISOString(), - data: data - } + data: data, + }, }); if (status !== "scraping") { @@ -174,7 +179,7 @@ async function crawlStatusWS( // Basically just middleware and error wrapping export async function crawlStatusWSController( ws: WebSocket, - req: RequestWithAuth + req: RequestWithAuth, ) { try { const auth = await authenticateUser(req, null, RateLimiterMode.CrawlStatus); @@ -182,7 +187,7 @@ export async function crawlStatusWSController( if (!auth.success) { return close(ws, 3000, { type: "error", - error: auth.error + error: auth.error, }); } @@ -201,7 +206,7 @@ export async function crawlStatusWSController( verbose = JSON.stringify({ message: err.message, name: err.name, - stack: err.stack + stack: err.stack, }); } } @@ -212,13 +217,13 @@ export async function crawlStatusWSController( ") -- ID " + id + " -- " + - verbose + verbose, ); return close(ws, 1011, { type: "error", error: "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + - id + id, }); } } diff --git a/apps/api/src/controllers/v1/crawl-status.ts b/apps/api/src/controllers/v1/crawl-status.ts index d88d26fb..59db16d8 100644 --- a/apps/api/src/controllers/v1/crawl-status.ts +++ b/apps/api/src/controllers/v1/crawl-status.ts @@ -3,7 +3,7 @@ import { CrawlStatusParams, CrawlStatusResponse, ErrorResponse, - RequestWithAuth + RequestWithAuth, } from "./types"; import { getCrawl, @@ -11,12 +11,12 @@ import { getCrawlJobs, getDoneJobsOrdered, getDoneJobsOrderedLength, - getThrottledJobs + getThrottledJobs, } from "../../lib/crawl-redis"; import { getScrapeQueue } from "../../services/queue-service"; import { supabaseGetJobById, - supabaseGetJobsById + supabaseGetJobsById, } from "../../lib/supabase-jobs"; import { configDotenv } from "dotenv"; import { Job, JobState } from "bullmq"; @@ -70,7 +70,7 @@ export async function getJobs(ids: string[]) { export async function crawlStatusController( req: RequestWithAuth, res: Response, - isBatch = false + isBatch = false, ) { const sc = await getCrawl(req.params.jobId); if (!sc) { @@ -90,7 +90,9 @@ export async function crawlStatusController( let jobIDs = await getCrawlJobs(req.params.jobId); let jobStatuses = await Promise.all( - jobIDs.map(async (x) => [x, await getScrapeQueue().getJobState(x)] as const) + jobIDs.map( + async (x) => [x, await getScrapeQueue().getJobState(x)] as const, + ), ); const throttledJobs = new Set(...(await getThrottledJobs(req.auth.team_id))); @@ -124,7 +126,7 @@ export async function crawlStatusController( const doneJobsOrder = await getDoneJobsOrdered( req.params.jobId, start, - end ?? -1 + end ?? -1, ); let doneJobs: Job[] = []; @@ -158,7 +160,7 @@ export async function crawlStatusController( if (job.returnvalue === undefined) { logger.warn( "Job was considered done, but returnvalue is undefined!", - { jobId: job.id, state } + { jobId: job.id, state }, ); continue; } @@ -175,8 +177,8 @@ export async function crawlStatusController( doneJobs = ( await Promise.all( (await getJobs(doneJobsOrder)).map(async (x) => - (await x.getState()) === "failed" ? null : x - ) + (await x.getState()) === "failed" ? null : x, + ), ) ).filter((x) => x !== null) as Job[]; } @@ -185,7 +187,7 @@ export async function crawlStatusController( const protocol = process.env.ENV === "local" ? req.protocol : "https"; const nextURL = new URL( - `${protocol}://${req.get("host")}/v1/${isBatch ? "batch/scrape" : "crawl"}/${req.params.jobId}` + `${protocol}://${req.get("host")}/v1/${isBatch ? "batch/scrape" : "crawl"}/${req.params.jobId}`, ); nextURL.searchParams.set("skip", (start + data.length).toString()); @@ -215,6 +217,6 @@ export async function crawlStatusController( status !== "scraping" && start + data.length === doneJobsLength // if there's not gonna be any documents after this ? undefined : nextURL.href, - data: data + data: data, }); } diff --git a/apps/api/src/controllers/v1/crawl.ts b/apps/api/src/controllers/v1/crawl.ts index dac1b735..1fb470f9 100644 --- a/apps/api/src/controllers/v1/crawl.ts +++ b/apps/api/src/controllers/v1/crawl.ts @@ -5,7 +5,7 @@ import { crawlRequestSchema, CrawlResponse, RequestWithAuth, - toLegacyCrawlerOptions + toLegacyCrawlerOptions, } from "./types"; import { addCrawlJob, @@ -14,7 +14,7 @@ import { lockURL, lockURLs, saveCrawl, - StoredCrawl + StoredCrawl, } from "../../lib/crawl-redis"; import { logCrawl } from "../../services/logging/crawl_log"; import { getScrapeQueue } from "../../services/queue-service"; @@ -26,7 +26,7 @@ import { scrapeOptions as scrapeOptionsSchema } from "./types"; export async function crawlController( req: RequestWithAuth<{}, CrawlResponse, CrawlRequest>, - res: Response + res: Response, ) { const preNormalizedBody = req.body; req.body = crawlRequestSchema.parse(req.body); @@ -37,12 +37,12 @@ export async function crawlController( module: "api/v1", method: "crawlController", teamId: req.auth.team_id, - plan: req.auth.plan + plan: req.auth.plan, }); logger.debug("Crawl " + id + " starting", { request: req.body, originalRequest: preNormalizedBody, - account: req.account + account: req.account, }); await logCrawl(id, req.auth.team_id); @@ -56,7 +56,7 @@ export async function crawlController( const crawlerOptions = { ...req.body, url: undefined, - scrapeOptions: undefined + scrapeOptions: undefined, }; const scrapeOptions = req.body.scrapeOptions; @@ -86,7 +86,7 @@ export async function crawlController( logger.debug("Determined limit: " + crawlerOptions.limit, { remainingCredits, bodyLimit: originalLimit, - originalBodyLimit: preNormalizedBody.limit + originalBodyLimit: preNormalizedBody.limit, }); const sc: StoredCrawl = { @@ -96,7 +96,7 @@ export async function crawlController( internalOptions: { disableSmartWaitCache: true }, // NOTE: smart wait disabled for crawls to ensure contentful scrape, speed does not matter team_id: req.auth.team_id, createdAt: Date.now(), - plan: req.auth.plan + plan: req.auth.plan, }; const crawler = crawlToCrawler(id, sc); @@ -105,7 +105,7 @@ export async function crawlController( sc.robots = await crawler.getRobotsTxt(scrapeOptions.skipTlsVerification); } catch (e) { logger.debug("Failed to get robots.txt (this is probably fine!)", { - error: e + error: e, }); } @@ -117,7 +117,7 @@ export async function crawlController( if (sitemap !== null && sitemap.length > 0) { logger.debug("Using sitemap of length " + sitemap.length, { - sitemapLength: sitemap.length + sitemapLength: sitemap.length, }); let jobPriority = 20; // If it is over 1000, we need to get the job priority, @@ -127,7 +127,7 @@ export async function crawlController( jobPriority = await getJobPriority({ plan: req.auth.plan, team_id: req.auth.team_id, - basePriority: 21 + basePriority: 21, }); } logger.debug("Using job priority " + jobPriority, { jobPriority }); @@ -149,12 +149,12 @@ export async function crawlController( crawl_id: id, sitemapped: true, webhook: req.body.webhook, - v1: true + v1: true, }, opts: { jobId: uuid, - priority: 20 - } + priority: 20, + }, }; }); @@ -162,18 +162,18 @@ export async function crawlController( await lockURLs( id, sc, - jobs.map((x) => x.data.url) + jobs.map((x) => x.data.url), ); logger.debug("Adding scrape jobs to Redis..."); await addCrawlJobs( id, - jobs.map((x) => x.opts.jobId) + jobs.map((x) => x.opts.jobId), ); logger.debug("Adding scrape jobs to BullMQ..."); await getScrapeQueue().addBulk(jobs); } else { logger.debug("Sitemap not found or ignored.", { - ignoreSitemap: sc.crawlerOptions.ignoreSitemap + ignoreSitemap: sc.crawlerOptions.ignoreSitemap, }); logger.debug("Locking URL..."); @@ -192,12 +192,12 @@ export async function crawlController( origin: "api", crawl_id: id, webhook: req.body.webhook, - v1: true + v1: true, }, { - priority: 15 + priority: 15, }, - jobId + jobId, ); logger.debug("Adding scrape job to BullMQ...", { jobId }); await addCrawlJob(id, jobId); @@ -206,7 +206,7 @@ export async function crawlController( if (req.body.webhook) { logger.debug("Calling webhook with crawl.started...", { - webhook: req.body.webhook + webhook: req.body.webhook, }); await callWebhook( req.auth.team_id, @@ -214,7 +214,7 @@ export async function crawlController( null, req.body.webhook, true, - "crawl.started" + "crawl.started", ); } @@ -223,6 +223,6 @@ export async function crawlController( return res.status(200).json({ success: true, id, - url: `${protocol}://${req.get("host")}/v1/crawl/${id}` + url: `${protocol}://${req.get("host")}/v1/crawl/${id}`, }); } diff --git a/apps/api/src/controllers/v1/extract.ts b/apps/api/src/controllers/v1/extract.ts index 74b188e7..0c286253 100644 --- a/apps/api/src/controllers/v1/extract.ts +++ b/apps/api/src/controllers/v1/extract.ts @@ -6,7 +6,7 @@ import { extractRequestSchema, ExtractResponse, MapDocument, - scrapeOptions + scrapeOptions, } from "./types"; import { Document } from "../../lib/entities"; import Redis from "ioredis"; @@ -43,7 +43,7 @@ const MIN_REQUIRED_LINKS = 1; */ export async function extractController( req: RequestWithAuth<{}, ExtractResponse, ExtractRequest>, - res: Response + res: Response, ) { const selfHosted = process.env.USE_DB_AUTHENTICATION !== "true"; @@ -81,7 +81,7 @@ export async function extractController( // If we're self-hosted, we don't want to ignore the sitemap, due to our fire-engine mapping ignoreSitemap: !selfHosted ? true : false, includeMetadata: true, - includeSubdomains: req.body.includeSubdomains + includeSubdomains: req.body.includeSubdomains, }); let mappedLinks = mapResults.links as MapDocument[]; @@ -89,7 +89,8 @@ export async function extractController( mappedLinks = mappedLinks.slice(0, MAX_EXTRACT_LIMIT); let mappedLinksRerank = mappedLinks.map( - (x) => `url: ${x.url}, title: ${x.title}, description: ${x.description}` + (x) => + `url: ${x.url}, title: ${x.title}, description: ${x.description}`, ); // Filter by path prefix if present @@ -103,31 +104,31 @@ export async function extractController( const linksAndScores = await performRanking( mappedLinksRerank, mappedLinks.map((l) => l.url), - mapUrl + mapUrl, ); // First try with high threshold let filteredLinks = filterAndProcessLinks( mappedLinks, linksAndScores, - INITIAL_SCORE_THRESHOLD + INITIAL_SCORE_THRESHOLD, ); // If we don't have enough high-quality links, try with lower threshold if (filteredLinks.length < MIN_REQUIRED_LINKS) { logger.info( - `Only found ${filteredLinks.length} links with score > ${INITIAL_SCORE_THRESHOLD}. Trying lower threshold...` + `Only found ${filteredLinks.length} links with score > ${INITIAL_SCORE_THRESHOLD}. Trying lower threshold...`, ); filteredLinks = filterAndProcessLinks( mappedLinks, linksAndScores, - FALLBACK_SCORE_THRESHOLD + FALLBACK_SCORE_THRESHOLD, ); if (filteredLinks.length === 0) { // If still no results, take top N results regardless of score logger.warn( - `No links found with score > ${FALLBACK_SCORE_THRESHOLD}. Taking top ${MIN_REQUIRED_LINKS} results.` + `No links found with score > ${FALLBACK_SCORE_THRESHOLD}. Taking top ${MIN_REQUIRED_LINKS} results.`, ); filteredLinks = linksAndScores .sort((a, b) => b.score - a.score) @@ -135,7 +136,9 @@ export async function extractController( .map((x) => mappedLinks.find((link) => link.url === x.link)) .filter( (x): x is MapDocument => - x !== undefined && x.url !== undefined && !isUrlBlocked(x.url) + x !== undefined && + x.url !== undefined && + !isUrlBlocked(x.url), ); } } @@ -161,7 +164,7 @@ export async function extractController( return res.status(400).json({ success: false, error: - "No valid URLs found to scrape. Try adjusting your search criteria or including more URLs." + "No valid URLs found to scrape. Try adjusting your search criteria or including more URLs.", }); } @@ -174,7 +177,7 @@ export async function extractController( const jobPriority = await getJobPriority({ plan: req.auth.plan as PlanType, team_id: req.auth.team_id, - basePriority: 10 + basePriority: 10, }); await addScrapeJob( @@ -186,11 +189,11 @@ export async function extractController( internalOptions: {}, plan: req.auth.plan!, origin, - is_scrape: true + is_scrape: true, }, {}, jobId, - jobPriority + jobPriority, ); try { @@ -208,12 +211,12 @@ export async function extractController( ) { throw { status: 408, - error: "Request timed out" + error: "Request timed out", }; } else { throw { status: 500, - error: `(Internal server error) - ${e && e.message ? e.message : e}` + error: `(Internal server error) - ${e && e.message ? e.message : e}`, }; } } @@ -225,7 +228,7 @@ export async function extractController( } catch (e) { return res.status(e.status).json({ success: false, - error: e.error + error: e.error, }); } @@ -237,11 +240,11 @@ export async function extractController( "Always prioritize using the provided content to answer the question. Do not make up an answer. Be concise and follow the schema if provided. Here are the urls the user provided of which he wants to extract information from: " + links.join(", "), prompt: req.body.prompt, - schema: req.body.schema + schema: req.body.schema, }, docs.map((x) => buildDocument(x)).join("\n"), undefined, - true // isExtractEndpoint + true, // isExtractEndpoint ); // TODO: change this later @@ -249,9 +252,9 @@ export async function extractController( billTeam(req.auth.team_id, req.acuc?.sub_id, links.length * 5).catch( (error) => { logger.error( - `Failed to bill team ${req.auth.team_id} for ${links.length * 5} credits: ${error}` + `Failed to bill team ${req.auth.team_id} for ${links.length * 5} credits: ${error}`, ); - } + }, ); let data = completions.extract ?? {}; @@ -269,14 +272,14 @@ export async function extractController( url: req.body.urls.join(", "), scrapeOptions: req.body, origin: req.body.origin ?? "api", - num_tokens: completions.numTokens ?? 0 + num_tokens: completions.numTokens ?? 0, }); return res.status(200).json({ success: true, data: data, scrape_id: id, - warning: warning + warning: warning, }); } @@ -295,13 +298,13 @@ function filterAndProcessLinks( score: number; originalIndex: number; }[], - threshold: number + threshold: number, ): MapDocument[] { return linksAndScores .filter((x) => x.score > threshold) .map((x) => mappedLinks.find((link) => link.url === x.link)) .filter( (x): x is MapDocument => - x !== undefined && x.url !== undefined && !isUrlBlocked(x.url) + x !== undefined && x.url !== undefined && !isUrlBlocked(x.url), ); } diff --git a/apps/api/src/controllers/v1/map.ts b/apps/api/src/controllers/v1/map.ts index 7ddd7b78..cd302708 100644 --- a/apps/api/src/controllers/v1/map.ts +++ b/apps/api/src/controllers/v1/map.ts @@ -4,7 +4,7 @@ import { MapDocument, mapRequestSchema, RequestWithAuth, - scrapeOptions + scrapeOptions, } from "./types"; import { crawlToCrawler, StoredCrawl } from "../../lib/crawl-redis"; import { MapResponse, MapRequest } from "./types"; @@ -13,7 +13,7 @@ import { checkAndUpdateURLForMap, isSameDomain, isSameSubdomain, - removeDuplicateUrls + removeDuplicateUrls, } from "../../lib/validateUrl"; import { fireEngineMap } from "../../search/fireEngine"; import { billTeam } from "../../services/billing/credit_billing"; @@ -49,7 +49,7 @@ export async function getMapResults({ plan, origin, includeMetadata = false, - allowExternalLinks + allowExternalLinks, }: { url: string; search?: string; @@ -72,13 +72,13 @@ export async function getMapResults({ crawlerOptions: { ...crawlerOptions, limit: crawlerOptions.sitemapOnly ? 10000000 : limit, - scrapeOptions: undefined + scrapeOptions: undefined, }, scrapeOptions: scrapeOptions.parse({}), internalOptions: {}, team_id: teamId, createdAt: Date.now(), - plan: plan + plan: plan, }; const crawler = crawlToCrawler(id, sc); @@ -114,7 +114,7 @@ export async function getMapResults({ const resultsPerPage = 100; const maxPages = Math.ceil( - Math.min(MAX_FIRE_ENGINE_RESULTS, limit) / resultsPerPage + Math.min(MAX_FIRE_ENGINE_RESULTS, limit) / resultsPerPage, ); const cacheKey = `fireEngineMap:${mapUrl}`; @@ -129,12 +129,12 @@ export async function getMapResults({ const fetchPage = async (page: number) => { return fireEngineMap(mapUrl, { numResults: resultsPerPage, - page: page + page: page, }); }; pagePromises = Array.from({ length: maxPages }, (_, i) => - fetchPage(i + 1) + fetchPage(i + 1), ); allResults = await Promise.all(pagePromises); @@ -144,7 +144,7 @@ export async function getMapResults({ // Parallelize sitemap fetch with serper search const [sitemap, ...searchResults] = await Promise.all([ ignoreSitemap ? null : crawler.tryGetSitemap(true), - ...(cachedResult ? [] : pagePromises) + ...(cachedResult ? [] : pagePromises), ]); if (!cachedResult) { @@ -172,7 +172,7 @@ export async function getMapResults({ links = [ mapResults[0].url, ...mapResults.slice(1).map((x) => x.url), - ...links + ...links, ]; } else { mapResults.map((x) => { @@ -218,13 +218,13 @@ export async function getMapResults({ links: includeMetadata ? mapResults : linksToReturn, scrape_id: origin?.includes("website") ? id : undefined, job_id: id, - time_taken: (new Date().getTime() - Date.now()) / 1000 + time_taken: (new Date().getTime() - Date.now()) / 1000, }; } export async function mapController( req: RequestWithAuth<{}, MapResponse, MapRequest>, - res: Response + res: Response, ) { req.body = mapRequestSchema.parse(req.body); @@ -237,13 +237,13 @@ export async function mapController( crawlerOptions: req.body, origin: req.body.origin, teamId: req.auth.team_id, - plan: req.auth.plan + plan: req.auth.plan, }); // Bill the team billTeam(req.auth.team_id, req.acuc?.sub_id, 1).catch((error) => { logger.error( - `Failed to bill team ${req.auth.team_id} for 1 credit: ${error}` + `Failed to bill team ${req.auth.team_id} for 1 credit: ${error}`, ); }); @@ -261,13 +261,13 @@ export async function mapController( crawlerOptions: {}, scrapeOptions: {}, origin: req.body.origin ?? "api", - num_tokens: 0 + num_tokens: 0, }); const response = { success: true as const, links: result.links, - scrape_id: result.scrape_id + scrape_id: result.scrape_id, }; return res.status(200).json(response); diff --git a/apps/api/src/controllers/v1/scrape-status.ts b/apps/api/src/controllers/v1/scrape-status.ts index b366b79e..7fec74a1 100644 --- a/apps/api/src/controllers/v1/scrape-status.ts +++ b/apps/api/src/controllers/v1/scrape-status.ts @@ -13,29 +13,29 @@ export async function scrapeStatusController(req: any, res: any) { const job = await supabaseGetJobByIdOnlyData(req.params.jobId); const allowedTeams = [ "41bdbfe1-0579-4d9b-b6d5-809f16be12f5", - "511544f2-2fce-4183-9c59-6c29b02c69b5" + "511544f2-2fce-4183-9c59-6c29b02c69b5", ]; if (!allowedTeams.includes(job?.team_id)) { return res.status(403).json({ success: false, - error: "You are not allowed to access this resource." + error: "You are not allowed to access this resource.", }); } return res.status(200).json({ success: true, - data: job?.docs[0] + data: job?.docs[0], }); } catch (error) { if (error instanceof Error && error.message == "Too Many Requests") { return res.status(429).json({ success: false, - error: "Rate limit exceeded. Please try again later." + error: "Rate limit exceeded. Please try again later.", }); } else { return res.status(500).json({ success: false, - error: "An unexpected error occurred." + error: "An unexpected error occurred.", }); } } diff --git a/apps/api/src/controllers/v1/scrape.ts b/apps/api/src/controllers/v1/scrape.ts index 05cc68e3..ddd5da74 100644 --- a/apps/api/src/controllers/v1/scrape.ts +++ b/apps/api/src/controllers/v1/scrape.ts @@ -5,7 +5,7 @@ import { RequestWithAuth, ScrapeRequest, scrapeRequestSchema, - ScrapeResponse + ScrapeResponse, } from "./types"; import { billTeam } from "../../services/billing/credit_billing"; import { v4 as uuidv4 } from "uuid"; @@ -17,7 +17,7 @@ import { getScrapeQueue } from "../../services/queue-service"; export async function scrapeController( req: RequestWithAuth<{}, ScrapeResponse, ScrapeRequest>, - res: Response + res: Response, ) { req.body = scrapeRequestSchema.parse(req.body); let earlyReturn = false; @@ -30,7 +30,7 @@ export async function scrapeController( const jobPriority = await getJobPriority({ plan: req.auth.plan as PlanType, team_id: req.auth.team_id, - basePriority: 10 + basePriority: 10, }); await addScrapeJob( @@ -42,18 +42,18 @@ export async function scrapeController( internalOptions: {}, plan: req.auth.plan!, origin: req.body.origin, - is_scrape: true + is_scrape: true, }, {}, jobId, - jobPriority + jobPriority, ); const totalWait = (req.body.waitFor ?? 0) + (req.body.actions ?? []).reduce( (a, x) => (x.type === "wait" ? (x.milliseconds ?? 0) : 0) + a, - 0 + 0, ); let doc: Document; @@ -67,12 +67,12 @@ export async function scrapeController( ) { return res.status(408).json({ success: false, - error: "Request timed out" + error: "Request timed out", }); } else { return res.status(500).json({ success: false, - error: `(Internal server error) - ${e && e.message ? e.message : e}` + error: `(Internal server error) - ${e && e.message ? e.message : e}`, }); } } @@ -99,10 +99,10 @@ export async function scrapeController( billTeam(req.auth.team_id, req.acuc?.sub_id, creditsToBeBilled).catch( (error) => { logger.error( - `Failed to bill team ${req.auth.team_id} for ${creditsToBeBilled} credits: ${error}` + `Failed to bill team ${req.auth.team_id} for ${creditsToBeBilled} credits: ${error}`, ); // Optionally, you could notify an admin or add to a retry queue here - } + }, ); if (!req.body.formats.includes("rawHtml")) { @@ -123,12 +123,12 @@ export async function scrapeController( url: req.body.url, scrapeOptions: req.body, origin: origin, - num_tokens: numTokens + num_tokens: numTokens, }); return res.status(200).json({ success: true, data: doc, - scrape_id: origin?.includes("website") ? jobId : undefined + scrape_id: origin?.includes("website") ? jobId : undefined, }); } diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index f9fa2392..57e208b4 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -8,7 +8,7 @@ import { ExtractorOptions, PageOptions, ScrapeActionContent, - Document as V0Document + Document as V0Document, } from "../../lib/entities"; import { InternalOptions } from "../../scraper/scrapeURL"; @@ -34,7 +34,7 @@ export const url = z.preprocess( .regex(/^https?:\/\//, "URL uses unsupported protocol") .refine( (x) => /\.[a-z]{2,}([\/?#]|$)/i.test(x), - "URL must have a valid top-level domain or be a valid path" + "URL must have a valid top-level domain or be a valid path", ) .refine((x) => { try { @@ -46,8 +46,8 @@ export const url = z.preprocess( }, "Invalid URL") .refine( (x) => !isUrlBlocked(x as string), - "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it." - ) + "Firecrawl currently does not support social media scraping due to policy restrictions. We're actively working on building support for it.", + ), ); const strictMessage = @@ -60,9 +60,9 @@ export const extractOptions = z systemPrompt: z .string() .default( - "Based on the information on the page, extract all the information from the schema in JSON format. Try to extract all the fields even those that might not be marked as required." + "Based on the information on the page, extract all the information from the schema in JSON format. Try to extract all the fields even those that might not be marked as required.", ), - prompt: z.string().optional() + prompt: z.string().optional(), }) .strict(strictMessage); @@ -74,7 +74,7 @@ export const actionsSchema = z.array( .object({ type: z.literal("wait"), milliseconds: z.number().int().positive().finite().optional(), - selector: z.string().optional() + selector: z.string().optional(), }) .refine( (data) => @@ -82,38 +82,38 @@ export const actionsSchema = z.array( !(data.milliseconds !== undefined && data.selector !== undefined), { message: - "Either 'milliseconds' or 'selector' must be provided, but not both." - } + "Either 'milliseconds' or 'selector' must be provided, but not both.", + }, ), z.object({ type: z.literal("click"), - selector: z.string() + selector: z.string(), }), z.object({ type: z.literal("screenshot"), - fullPage: z.boolean().default(false) + fullPage: z.boolean().default(false), }), z.object({ type: z.literal("write"), - text: z.string() + text: z.string(), }), z.object({ type: z.literal("press"), - key: z.string() + key: z.string(), }), z.object({ type: z.literal("scroll"), direction: z.enum(["up", "down"]).optional().default("down"), - selector: z.string().optional() + selector: z.string().optional(), }), z.object({ - type: z.literal("scrape") + type: z.literal("scrape"), }), z.object({ type: z.literal("executeJavascript"), - script: z.string() - }) - ]) + script: z.string(), + }), + ]), ); export const scrapeOptions = z @@ -126,14 +126,14 @@ export const scrapeOptions = z "links", "screenshot", "screenshot@fullPage", - "extract" + "extract", ]) .array() .optional() .default(["markdown"]) .refine( (x) => !(x.includes("screenshot") && x.includes("screenshot@fullPage")), - "You may only specify either screenshot or screenshot@fullPage" + "You may only specify either screenshot or screenshot@fullPage", ), headers: z.record(z.string(), z.string()).optional(), includeTags: z.string().array().optional(), @@ -155,11 +155,11 @@ export const scrapeOptions = z (val) => !val || Object.keys(countries).includes(val.toUpperCase()), { message: - "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code." - } + "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code.", + }, ) .transform((val) => (val ? val.toUpperCase() : "US")), - languages: z.string().array().optional() + languages: z.string().array().optional(), }) .optional(), @@ -173,15 +173,15 @@ export const scrapeOptions = z (val) => !val || Object.keys(countries).includes(val.toUpperCase()), { message: - "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code." - } + "Invalid country code. Please use a valid ISO 3166-1 alpha-2 country code.", + }, ) .transform((val) => (val ? val.toUpperCase() : "US")), - languages: z.string().array().optional() + languages: z.string().array().optional(), }) .optional(), skipTlsVerification: z.boolean().default(false), - removeBase64Images: z.boolean().default(true) + removeBase64Images: z.boolean().default(true), }) .strict(strictMessage); @@ -199,7 +199,7 @@ export const extractV1Options = z includeSubdomains: z.boolean().default(true), allowExternalLinks: z.boolean().default(false), origin: z.string().optional().default("api"), - timeout: z.number().int().positive().finite().safe().default(60000) + timeout: z.number().int().positive().finite().safe().default(60000), }) .strict(strictMessage); @@ -212,7 +212,7 @@ export const scrapeRequestSchema = scrapeOptions .extend({ url, origin: z.string().optional().default("api"), - timeout: z.number().int().positive().finite().safe().default(30000) + timeout: z.number().int().positive().finite().safe().default(30000), }) .strict(strictMessage) .refine( @@ -226,8 +226,8 @@ export const scrapeRequestSchema = scrapeOptions }, { message: - "When 'extract' format is specified, 'extract' options must be provided, and vice versa" - } + "When 'extract' format is specified, 'extract' options must be provided, and vice versa", + }, ) .transform((obj) => { if ((obj.formats?.includes("extract") || obj.extract) && !obj.timeout) { @@ -250,9 +250,9 @@ export const webhookSchema = z.preprocess( z .object({ url: z.string().url(), - headers: z.record(z.string(), z.string()).default({}) + headers: z.record(z.string(), z.string()).default({}), }) - .strict(strictMessage) + .strict(strictMessage), ); export const batchScrapeRequestSchema = scrapeOptions @@ -260,7 +260,7 @@ export const batchScrapeRequestSchema = scrapeOptions urls: url.array(), origin: z.string().optional().default("api"), webhook: webhookSchema.optional(), - appendToId: z.string().uuid().optional() + appendToId: z.string().uuid().optional(), }) .strict(strictMessage) .refine( @@ -274,8 +274,8 @@ export const batchScrapeRequestSchema = scrapeOptions }, { message: - "When 'extract' format is specified, 'extract' options must be provided, and vice versa" - } + "When 'extract' format is specified, 'extract' options must be provided, and vice versa", + }, ); export type BatchScrapeRequest = z.infer; @@ -292,7 +292,7 @@ const crawlerOptions = z ignoreRobotsTxt: z.boolean().default(false), ignoreSitemap: z.boolean().default(false), deduplicateSimilarURLs: z.boolean().default(true), - ignoreQueryParameters: z.boolean().default(false) + ignoreQueryParameters: z.boolean().default(false), }) .strict(strictMessage); @@ -314,7 +314,7 @@ export const crawlRequestSchema = crawlerOptions origin: z.string().optional().default("api"), scrapeOptions: scrapeOptions.default({}), webhook: webhookSchema.optional(), - limit: z.number().default(10000) + limit: z.number().default(10000), }) .strict(strictMessage); @@ -340,7 +340,7 @@ export const mapRequestSchema = crawlerOptions search: z.string().optional(), ignoreSitemap: z.boolean().default(false), sitemapOnly: z.boolean().default(false), - limit: z.number().min(1).max(5000).default(5000) + limit: z.number().min(1).max(5000).default(5000), }) .strict(strictMessage); @@ -510,7 +510,7 @@ export type AuthCreditUsageChunk = { export interface RequestWithMaybeACUC< ReqParams = {}, ReqBody = undefined, - ResBody = undefined + ResBody = undefined, > extends Request { acuc?: AuthCreditUsageChunk; } @@ -518,7 +518,7 @@ export interface RequestWithMaybeACUC< export interface RequestWithACUC< ReqParams = {}, ReqBody = undefined, - ResBody = undefined + ResBody = undefined, > extends Request { acuc: AuthCreditUsageChunk; } @@ -526,7 +526,7 @@ export interface RequestWithACUC< export interface RequestWithAuth< ReqParams = {}, ReqBody = undefined, - ResBody = undefined + ResBody = undefined, > extends Request { auth: AuthObject; account?: Account; @@ -535,7 +535,7 @@ export interface RequestWithAuth< export interface RequestWithMaybeAuth< ReqParams = {}, ReqBody = undefined, - ResBody = undefined + ResBody = undefined, > extends RequestWithMaybeACUC { auth?: AuthObject; account?: Account; @@ -544,7 +544,7 @@ export interface RequestWithMaybeAuth< export interface RequestWithAuth< ReqParams = {}, ReqBody = undefined, - ResBody = undefined + ResBody = undefined, > extends RequestWithACUC { auth: AuthObject; account?: Account; @@ -569,7 +569,7 @@ export function toLegacyCrawlerOptions(x: CrawlerOptions) { ignoreRobotsTxt: x.ignoreRobotsTxt, ignoreSitemap: x.ignoreSitemap, deduplicateSimilarURLs: x.deduplicateSimilarURLs, - ignoreQueryParameters: x.ignoreQueryParameters + ignoreQueryParameters: x.ignoreQueryParameters, }; } @@ -589,11 +589,11 @@ export function fromLegacyCrawlerOptions(x: any): { ignoreRobotsTxt: x.ignoreRobotsTxt, ignoreSitemap: x.ignoreSitemap, deduplicateSimilarURLs: x.deduplicateSimilarURLs, - ignoreQueryParameters: x.ignoreQueryParameters + ignoreQueryParameters: x.ignoreQueryParameters, }), internalOptions: { - v0CrawlOnlyUrls: x.returnOnlyUrls - } + v0CrawlOnlyUrls: x.returnOnlyUrls, + }, }; } @@ -605,7 +605,7 @@ export interface MapDocument { export function fromLegacyScrapeOptions( pageOptions: PageOptions, extractorOptions: ExtractorOptions | undefined, - timeout: number | undefined + timeout: number | undefined, ): { scrapeOptions: ScrapeOptions; internalOptions: InternalOptions } { return { scrapeOptions: scrapeOptions.parse({ @@ -621,7 +621,7 @@ export function fromLegacyScrapeOptions( extractorOptions.mode.includes("llm-extraction") ? ("extract" as const) : null, - "links" + "links", ].filter((x) => x !== null), waitFor: pageOptions.waitFor, headers: pageOptions.headers, @@ -646,16 +646,16 @@ export function fromLegacyScrapeOptions( ? { systemPrompt: extractorOptions.extractionPrompt, prompt: extractorOptions.userPrompt, - schema: extractorOptions.extractionSchema + schema: extractorOptions.extractionSchema, } : undefined, - mobile: pageOptions.mobile + mobile: pageOptions.mobile, }), internalOptions: { atsv: pageOptions.atsv, v0DisableJsDom: pageOptions.disableJsDom, - v0UseFastMode: pageOptions.useFastMode - } + v0UseFastMode: pageOptions.useFastMode, + }, // TODO: fallback, fetchPageContent, replaceAllPathsWithAbsolutePaths, includeLinks }; } @@ -664,12 +664,12 @@ export function fromLegacyCombo( pageOptions: PageOptions, extractorOptions: ExtractorOptions | undefined, timeout: number | undefined, - crawlerOptions: any + crawlerOptions: any, ): { scrapeOptions: ScrapeOptions; internalOptions: InternalOptions } { const { scrapeOptions, internalOptions: i1 } = fromLegacyScrapeOptions( pageOptions, extractorOptions, - timeout + timeout, ); const { internalOptions: i2 } = fromLegacyCrawlerOptions(crawlerOptions); return { scrapeOptions, internalOptions: Object.assign(i1, i2) }; @@ -677,7 +677,7 @@ export function fromLegacyCombo( export function toLegacyDocument( document: Document, - internalOptions: InternalOptions + internalOptions: InternalOptions, ): V0Document | { url: string } { if (internalOptions.v0CrawlOnlyUrls) { return { url: document.metadata.sourceURL! }; @@ -696,9 +696,9 @@ export function toLegacyDocument( statusCode: undefined, pageError: document.metadata.error, pageStatusCode: document.metadata.statusCode, - screenshot: document.screenshot + screenshot: document.screenshot, }, actions: document.actions, - warning: document.warning + warning: document.warning, }; } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a4f4445b..adc080f2 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -46,12 +46,12 @@ serverAdapter.setBasePath(`/admin/${process.env.BULL_AUTH_KEY}/queues`); const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard({ queues: [new BullAdapter(getScrapeQueue())], - serverAdapter: serverAdapter + serverAdapter: serverAdapter, }); app.use( `/admin/${process.env.BULL_AUTH_KEY}/queues`, - serverAdapter.getRouter() + serverAdapter.getRouter(), ); app.get("/", (req, res) => { @@ -75,7 +75,7 @@ function startServer(port = DEFAULT_PORT) { const server = app.listen(Number(port), HOST, () => { logger.info(`Worker ${process.pid} listening on port ${port}`); logger.info( - `For the Queue UI, open: http://${HOST}:${port}/admin/${process.env.BULL_AUTH_KEY}/queues` + `For the Queue UI, open: http://${HOST}:${port}/admin/${process.env.BULL_AUTH_KEY}/queues`, ); }); @@ -103,7 +103,7 @@ app.get(`/serverHealthCheck`, async (req, res) => { const noWaitingJobs = waitingJobs === 0; // 200 if no active jobs, 503 if there are active jobs return res.status(noWaitingJobs ? 200 : 500).json({ - waitingJobs + waitingJobs, }); } catch (error) { Sentry.captureException(error); @@ -120,7 +120,7 @@ app.get("/serverHealthCheck/notify", async (req, res) => { const getWaitingJobsCount = async () => { const scrapeQueue = getScrapeQueue(); const [waitingJobsCount] = await Promise.all([ - scrapeQueue.getWaitingCount() + scrapeQueue.getWaitingCount(), ]); return waitingJobsCount; @@ -140,15 +140,15 @@ app.get("/serverHealthCheck/notify", async (req, res) => { const message = { text: `⚠️ Warning: The number of active jobs (${waitingJobsCount}) has exceeded the threshold (${treshold}) for more than ${ timeout / 60000 - } minute(s).` + } minute(s).`, }; const response = await fetch(slackWebhookUrl, { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify(message) + body: JSON.stringify(message), }); if (!response.ok) { @@ -176,7 +176,7 @@ app.use( err: unknown, req: Request<{}, ErrorResponse, undefined>, res: Response, - next: NextFunction + next: NextFunction, ) => { if (err instanceof ZodError) { if ( @@ -192,7 +192,7 @@ app.use( } else { next(err); } - } + }, ); Sentry.setupExpressErrorHandler(app); @@ -202,7 +202,7 @@ app.use( err: unknown, req: Request<{}, ErrorResponse, undefined>, res: ResponseWithSentry, - next: NextFunction + next: NextFunction, ) => { if ( err instanceof SyntaxError && @@ -222,7 +222,7 @@ app.use( verbose = JSON.stringify({ message: err.message, name: err.name, - stack: err.stack + stack: err.stack, }); } } @@ -233,15 +233,15 @@ app.use( ") -- ID " + id + " -- " + - verbose + verbose, ); res.status(500).json({ success: false, error: "An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " + - id + id, }); - } + }, ); logger.info(`Worker ${process.pid} started`); diff --git a/apps/api/src/lib/LLM-extraction/index.ts b/apps/api/src/lib/LLM-extraction/index.ts index 47ecaf18..de7017ea 100644 --- a/apps/api/src/lib/LLM-extraction/index.ts +++ b/apps/api/src/lib/LLM-extraction/index.ts @@ -10,7 +10,7 @@ import { logger } from "../logger"; export async function generateCompletions( documents: Document[], extractionOptions: ExtractorOptions | undefined, - mode: "markdown" | "raw-html" + mode: "markdown" | "raw-html", ): Promise { // const schema = zodToJsonSchema(options.schema) @@ -32,7 +32,7 @@ export async function generateCompletions( schema: schema, prompt: prompt, systemPrompt: systemPrompt, - mode: mode + mode: mode, }); // Validate the JSON output against the schema using AJV if (schema) { @@ -43,8 +43,8 @@ export async function generateCompletions( `JSON parsing error(s): ${validate.errors ?.map((err) => err.message) .join( - ", " - )}\n\nLLM extraction did not match the extraction schema you provided. This could be because of a model hallucination, or an Error on our side. Try adjusting your prompt, and if it doesn't work reach out to support.` + ", ", + )}\n\nLLM extraction did not match the extraction schema you provided. This could be because of a model hallucination, or an Error on our side. Try adjusting your prompt, and if it doesn't work reach out to support.`, ); } } @@ -57,7 +57,7 @@ export async function generateCompletions( default: throw new Error("Invalid client"); } - }) + }), ); return completions; diff --git a/apps/api/src/lib/LLM-extraction/models.ts b/apps/api/src/lib/LLM-extraction/models.ts index 563863c0..cc1355de 100644 --- a/apps/api/src/lib/LLM-extraction/models.ts +++ b/apps/api/src/lib/LLM-extraction/models.ts @@ -14,7 +14,7 @@ const defaultPrompt = function prepareOpenAIDoc( document: Document, - mode: "markdown" | "raw-html" + mode: "markdown" | "raw-html", ): [OpenAI.Chat.Completions.ChatCompletionContentPart[], number] | null { let markdown = document.markdown; @@ -50,7 +50,7 @@ export async function generateOpenAICompletions({ systemPrompt = defaultPrompt, prompt, temperature, - mode + mode, }: { client: OpenAI; model?: string; @@ -68,7 +68,7 @@ export async function generateOpenAICompletions({ return { ...document, warning: - "LLM extraction was not performed since the document's content is empty or missing." + "LLM extraction was not performed since the document's content is empty or missing.", }; } const [content, numTokens] = preparedDoc; @@ -81,21 +81,21 @@ export async function generateOpenAICompletions({ messages: [ { role: "system", - content: systemPrompt + content: systemPrompt, }, { role: "user", content }, { role: "user", - content: `Transform the above content into structured json output based on the following user request: ${prompt}` - } + content: `Transform the above content into structured json output based on the following user request: ${prompt}`, + }, ], response_format: { type: "json_object" }, - temperature + temperature, }); try { llmExtraction = JSON.parse( - (jsonCompletion.choices[0].message.content ?? "").trim() + (jsonCompletion.choices[0].message.content ?? "").trim(), ); } catch (e) { throw new Error("Invalid JSON"); @@ -106,9 +106,9 @@ export async function generateOpenAICompletions({ messages: [ { role: "system", - content: systemPrompt + content: systemPrompt, }, - { role: "user", content } + { role: "user", content }, ], tools: [ { @@ -116,12 +116,12 @@ export async function generateOpenAICompletions({ function: { name: "extract_content", description: "Extracts the content from the given webpage(s)", - parameters: schema - } - } + parameters: schema, + }, + }, ], tool_choice: { type: "function", function: { name: "extract_content" } }, - temperature + temperature, }); const c = completion.choices[0].message.tool_calls[0].function.arguments; @@ -140,6 +140,6 @@ export async function generateOpenAICompletions({ warning: numTokens > maxTokens ? `Page was trimmed to fit the maximum token limit defined by the LLM model (Max: ${maxTokens} tokens, Attemped: ${numTokens} tokens). If results are not good, email us at help@mendable.ai so we can help you.` - : undefined + : undefined, }; } diff --git a/apps/api/src/lib/__tests__/html-to-markdown.test.ts b/apps/api/src/lib/__tests__/html-to-markdown.test.ts index f69c2949..d35e2cce 100644 --- a/apps/api/src/lib/__tests__/html-to-markdown.test.ts +++ b/apps/api/src/lib/__tests__/html-to-markdown.test.ts @@ -31,16 +31,16 @@ describe("parseMarkdown", () => { { html: "

Unclosed tag", expected: "Unclosed tag" }, { html: "

Missing closing div", - expected: "Missing closing div" + expected: "Missing closing div", }, { html: "

Wrong nesting

", - expected: "**Wrong nesting**" + expected: "**Wrong nesting**", }, { html: 'Link without closing tag', - expected: "[Link without closing tag](http://example.com)" - } + expected: "[Link without closing tag](http://example.com)", + }, ]; for (const { html, expected } of invalidHtmls) { diff --git a/apps/api/src/lib/__tests__/job-priority.test.ts b/apps/api/src/lib/__tests__/job-priority.test.ts index 4bd5fda9..1a7550ef 100644 --- a/apps/api/src/lib/__tests__/job-priority.test.ts +++ b/apps/api/src/lib/__tests__/job-priority.test.ts @@ -1,7 +1,7 @@ import { getJobPriority, addJobPriority, - deleteJobPriority + deleteJobPriority, } from "../job-priority"; import { redisConnection } from "../../services/queue-service"; import { PlanType } from "../../types"; @@ -11,8 +11,8 @@ jest.mock("../../services/queue-service", () => ({ sadd: jest.fn(), srem: jest.fn(), scard: jest.fn(), - expire: jest.fn() - } + expire: jest.fn(), + }, })); describe("Job Priority Tests", () => { @@ -26,11 +26,11 @@ describe("Job Priority Tests", () => { await addJobPriority(team_id, job_id); expect(redisConnection.sadd).toHaveBeenCalledWith( `limit_team_id:${team_id}`, - job_id + job_id, ); expect(redisConnection.expire).toHaveBeenCalledWith( `limit_team_id:${team_id}`, - 60 + 60, ); }); @@ -40,7 +40,7 @@ describe("Job Priority Tests", () => { await deleteJobPriority(team_id, job_id); expect(redisConnection.srem).toHaveBeenCalledWith( `limit_team_id:${team_id}`, - job_id + job_id, ); }); @@ -89,7 +89,7 @@ describe("Job Priority Tests", () => { await addJobPriority(team_id, job_id1); expect(redisConnection.expire).toHaveBeenCalledWith( `limit_team_id:${team_id}`, - 60 + 60, ); // Clear the mock calls @@ -99,7 +99,7 @@ describe("Job Priority Tests", () => { await addJobPriority(team_id, job_id2); expect(redisConnection.expire).toHaveBeenCalledWith( `limit_team_id:${team_id}`, - 60 + 60, ); }); @@ -112,7 +112,7 @@ describe("Job Priority Tests", () => { await addJobPriority(team_id, job_id); expect(redisConnection.expire).toHaveBeenCalledWith( `limit_team_id:${team_id}`, - 60 + 60, ); // Fast-forward time by 59 seconds diff --git a/apps/api/src/lib/batch-process.ts b/apps/api/src/lib/batch-process.ts index 20bb4ab6..1e4ac7be 100644 --- a/apps/api/src/lib/batch-process.ts +++ b/apps/api/src/lib/batch-process.ts @@ -1,7 +1,7 @@ export async function batchProcess( array: T[], batchSize: number, - asyncFunction: (item: T, index: number) => Promise + asyncFunction: (item: T, index: number) => Promise, ): Promise { const batches: T[][] = []; for (let i = 0; i < array.length; i += batchSize) { diff --git a/apps/api/src/lib/cache.ts b/apps/api/src/lib/cache.ts index 30c9f0b4..7dcbf88b 100644 --- a/apps/api/src/lib/cache.ts +++ b/apps/api/src/lib/cache.ts @@ -6,14 +6,14 @@ const logger = _logger.child({ module: "cache" }); export const cacheRedis = process.env.CACHE_REDIS_URL ? new IORedis(process.env.CACHE_REDIS_URL, { - maxRetriesPerRequest: null + maxRetriesPerRequest: null, }) : null; export function cacheKey( url: string, scrapeOptions: ScrapeOptions, - internalOptions: InternalOptions + internalOptions: InternalOptions, ): string | null { if (!cacheRedis) return null; @@ -49,7 +49,7 @@ export async function saveEntryToCache(key: string, entry: CacheEntry) { } export async function getEntryFromCache( - key: string + key: string, ): Promise { if (!cacheRedis) return null; diff --git a/apps/api/src/lib/concurrency-limit.ts b/apps/api/src/lib/concurrency-limit.ts index aba1fd3a..8205113f 100644 --- a/apps/api/src/lib/concurrency-limit.ts +++ b/apps/api/src/lib/concurrency-limit.ts @@ -14,37 +14,37 @@ export function getConcurrencyLimitMax(plan: string): number { export async function cleanOldConcurrencyLimitEntries( team_id: string, - now: number = Date.now() + now: number = Date.now(), ) { await redisConnection.zremrangebyscore(constructKey(team_id), -Infinity, now); } export async function getConcurrencyLimitActiveJobs( team_id: string, - now: number = Date.now() + now: number = Date.now(), ): Promise { return await redisConnection.zrangebyscore( constructKey(team_id), now, - Infinity + Infinity, ); } export async function pushConcurrencyLimitActiveJob( team_id: string, id: string, - now: number = Date.now() + now: number = Date.now(), ) { await redisConnection.zadd( constructKey(team_id), now + stalledJobTimeoutMs, - id + id, ); } export async function removeConcurrencyLimitActiveJob( team_id: string, - id: string + id: string, ) { await redisConnection.zrem(constructKey(team_id), id); } @@ -57,7 +57,7 @@ export type ConcurrencyLimitedJob = { }; export async function takeConcurrencyLimitedJob( - team_id: string + team_id: string, ): Promise { const res = await redisConnection.zmpop(1, constructQueueKey(team_id), "MIN"); if (res === null || res === undefined) { @@ -69,11 +69,11 @@ export async function takeConcurrencyLimitedJob( export async function pushConcurrencyLimitedJob( team_id: string, - job: ConcurrencyLimitedJob + job: ConcurrencyLimitedJob, ) { await redisConnection.zadd( constructQueueKey(team_id), job.priority ?? 1, - JSON.stringify(job) + JSON.stringify(job), ); } diff --git a/apps/api/src/lib/crawl-redis.test.ts b/apps/api/src/lib/crawl-redis.test.ts index ef2dabee..65d4e13a 100644 --- a/apps/api/src/lib/crawl-redis.test.ts +++ b/apps/api/src/lib/crawl-redis.test.ts @@ -3,7 +3,7 @@ import { generateURLPermutations } from "./crawl-redis"; describe("generateURLPermutations", () => { it("generates permutations correctly", () => { const bareHttps = generateURLPermutations("https://firecrawl.dev").map( - (x) => x.href + (x) => x.href, ); expect(bareHttps.length).toBe(4); expect(bareHttps.includes("https://firecrawl.dev/")).toBe(true); @@ -12,7 +12,7 @@ describe("generateURLPermutations", () => { expect(bareHttps.includes("http://www.firecrawl.dev/")).toBe(true); const bareHttp = generateURLPermutations("http://firecrawl.dev").map( - (x) => x.href + (x) => x.href, ); expect(bareHttp.length).toBe(4); expect(bareHttp.includes("https://firecrawl.dev/")).toBe(true); @@ -21,7 +21,7 @@ describe("generateURLPermutations", () => { expect(bareHttp.includes("http://www.firecrawl.dev/")).toBe(true); const wwwHttps = generateURLPermutations("https://www.firecrawl.dev").map( - (x) => x.href + (x) => x.href, ); expect(wwwHttps.length).toBe(4); expect(wwwHttps.includes("https://firecrawl.dev/")).toBe(true); @@ -30,7 +30,7 @@ describe("generateURLPermutations", () => { expect(wwwHttps.includes("http://www.firecrawl.dev/")).toBe(true); const wwwHttp = generateURLPermutations("http://www.firecrawl.dev").map( - (x) => x.href + (x) => x.href, ); expect(wwwHttp.length).toBe(4); expect(wwwHttp.includes("https://firecrawl.dev/")).toBe(true); diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index ab1a238d..6ccb9436 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -24,7 +24,7 @@ export async function saveCrawl(id: string, crawl: StoredCrawl) { method: "saveCrawl", crawlId: id, teamId: crawl.team_id, - plan: crawl.plan + plan: crawl.plan, }); await redisConnection.set("crawl:" + id, JSON.stringify(crawl)); await redisConnection.expire("crawl:" + id, 24 * 60 * 60, "NX"); @@ -53,7 +53,7 @@ export async function addCrawlJob(id: string, job_id: string) { jobId: job_id, module: "crawl-redis", method: "addCrawlJob", - crawlId: id + crawlId: id, }); await redisConnection.sadd("crawl:" + id + ":jobs", job_id); await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); @@ -64,7 +64,7 @@ export async function addCrawlJobs(id: string, job_ids: string[]) { jobIds: job_ids, module: "crawl-redis", method: "addCrawlJobs", - crawlId: id + crawlId: id, }); await redisConnection.sadd("crawl:" + id + ":jobs", ...job_ids); await redisConnection.expire("crawl:" + id + ":jobs", 24 * 60 * 60, "NX"); @@ -73,19 +73,19 @@ export async function addCrawlJobs(id: string, job_ids: string[]) { export async function addCrawlJobDone( id: string, job_id: string, - success: boolean + success: boolean, ) { _logger.debug("Adding done crawl job to Redis...", { jobId: job_id, module: "crawl-redis", method: "addCrawlJobDone", - crawlId: id + crawlId: id, }); await redisConnection.sadd("crawl:" + id + ":jobs_done", job_id); await redisConnection.expire( "crawl:" + id + ":jobs_done", 24 * 60 * 60, - "NX" + "NX", ); if (success) { @@ -93,7 +93,7 @@ export async function addCrawlJobDone( await redisConnection.expire( "crawl:" + id + ":jobs_done_ordered", 24 * 60 * 60, - "NX" + "NX", ); } } @@ -105,12 +105,12 @@ export async function getDoneJobsOrderedLength(id: string): Promise { export async function getDoneJobsOrdered( id: string, start = 0, - end = -1 + end = -1, ): Promise { return await redisConnection.lrange( "crawl:" + id + ":jobs_done_ordered", start, - end + end, ); } @@ -130,7 +130,7 @@ export async function finishCrawl(id: string) { _logger.debug("Marking crawl as finished.", { module: "crawl-redis", method: "finishCrawl", - crawlId: id + crawlId: id, }); const set = await redisConnection.setnx("crawl:" + id + ":finish", "yes"); if (set === 1) { @@ -141,7 +141,7 @@ export async function finishCrawl(id: string) { _logger.debug("Crawl can not be finished yet, not marking as finished.", { module: "crawl-redis", method: "finishCrawl", - crawlId: id + crawlId: id, }); } } @@ -154,7 +154,7 @@ export async function getThrottledJobs(teamId: string): Promise { return await redisConnection.zrangebyscore( "concurrency-limiter:" + teamId + ":throttled", Date.now(), - Infinity + Infinity, ); } @@ -201,7 +201,7 @@ export function generateURLPermutations(url: string | URL): URL[] { export async function lockURL( id: string, sc: StoredCrawl, - url: string + url: string, ): Promise { let logger = _logger.child({ crawlId: id, @@ -209,7 +209,7 @@ export async function lockURL( method: "lockURL", preNormalizedURL: url, teamId: sc.team_id, - plan: sc.plan + plan: sc.plan, }); if (typeof sc.crawlerOptions?.limit === "number") { @@ -218,7 +218,7 @@ export async function lockURL( sc.crawlerOptions.limit ) { logger.debug( - "Crawl has already hit visited_unique limit, not locking URL." + "Crawl has already hit visited_unique limit, not locking URL.", ); return false; } @@ -231,7 +231,7 @@ export async function lockURL( await redisConnection.expire( "crawl:" + id + ":visited_unique", 24 * 60 * 60, - "NX" + "NX", ); let res: boolean; @@ -242,7 +242,7 @@ export async function lockURL( // logger.debug("Adding URL permutations for URL " + JSON.stringify(url) + "...", { permutations }); const x = await redisConnection.sadd( "crawl:" + id + ":visited", - ...permutations + ...permutations, ); res = x === permutations.length; } @@ -250,7 +250,7 @@ export async function lockURL( await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); logger.debug("Locking URL " + JSON.stringify(url) + "... result: " + res, { - res + res, }); return res; } @@ -259,7 +259,7 @@ export async function lockURL( export async function lockURLs( id: string, sc: StoredCrawl, - urls: string[] + urls: string[], ): Promise { urls = urls.map((url) => normalizeURL(url, sc)); const logger = _logger.child({ @@ -267,7 +267,7 @@ export async function lockURLs( module: "crawl-redis", method: "lockURL", teamId: sc.team_id, - plan: sc.plan + plan: sc.plan, }); // Add to visited_unique set @@ -276,7 +276,7 @@ export async function lockURLs( await redisConnection.expire( "crawl:" + id + ":visited_unique", 24 * 60 * 60, - "NX" + "NX", ); let res: boolean; @@ -285,12 +285,12 @@ export async function lockURLs( res = x === urls.length; } else { const allPermutations = urls.flatMap((url) => - generateURLPermutations(url).map((x) => x.href) + generateURLPermutations(url).map((x) => x.href), ); logger.debug("Adding " + allPermutations.length + " URL permutations..."); const x = await redisConnection.sadd( "crawl:" + id + ":visited", - ...allPermutations + ...allPermutations, ); res = x === allPermutations.length; } @@ -304,7 +304,7 @@ export async function lockURLs( export function crawlToCrawler( id: string, sc: StoredCrawl, - newBase?: string + newBase?: string, ): WebCrawler { const crawler = new WebCrawler({ jobId: id, @@ -315,7 +315,7 @@ export function crawlToCrawler( maxCrawledLinks: sc.crawlerOptions?.maxCrawledLinks ?? 1000, maxCrawledDepth: getAdjustedMaxDepth( sc.originUrl!, - sc.crawlerOptions?.maxDepth ?? 10 + sc.crawlerOptions?.maxDepth ?? 10, ), limit: sc.crawlerOptions?.limit ?? 10000, generateImgAltText: sc.crawlerOptions?.generateImgAltText ?? false, @@ -323,7 +323,7 @@ export function crawlToCrawler( allowExternalContentLinks: sc.crawlerOptions?.allowExternalContentLinks ?? false, allowSubdomains: sc.crawlerOptions?.allowSubdomains ?? false, - ignoreRobotsTxt: sc.crawlerOptions?.ignoreRobotsTxt ?? false + ignoreRobotsTxt: sc.crawlerOptions?.ignoreRobotsTxt ?? false, }); if (sc.robots !== undefined) { diff --git a/apps/api/src/lib/custom-error.ts b/apps/api/src/lib/custom-error.ts index 25502a8e..20a01cb6 100644 --- a/apps/api/src/lib/custom-error.ts +++ b/apps/api/src/lib/custom-error.ts @@ -8,7 +8,7 @@ export class CustomError extends Error { statusCode: number, status: string, message: string = "", - dataIngestionJob?: any + dataIngestionJob?: any, ) { super(message); this.statusCode = statusCode; diff --git a/apps/api/src/lib/default-values.ts b/apps/api/src/lib/default-values.ts index ceca176c..2754b7cd 100644 --- a/apps/api/src/lib/default-values.ts +++ b/apps/api/src/lib/default-values.ts @@ -8,21 +8,21 @@ export const defaultPageOptions = { waitFor: 0, screenshot: false, fullPageScreenshot: false, - parsePDF: true + parsePDF: true, }; export const defaultCrawlerOptions = { allowBackwardCrawling: false, - limit: 10000 + limit: 10000, }; export const defaultCrawlPageOptions = { onlyMainContent: false, includeHtml: false, removeTags: [], - parsePDF: true + parsePDF: true, }; export const defaultExtractorOptions = { - mode: "markdown" + mode: "markdown", }; diff --git a/apps/api/src/lib/extract/reranker.ts b/apps/api/src/lib/extract/reranker.ts index 044f71a4..26e7ac06 100644 --- a/apps/api/src/lib/extract/reranker.ts +++ b/apps/api/src/lib/extract/reranker.ts @@ -1,21 +1,21 @@ import { CohereClient } from "cohere-ai"; import { MapDocument } from "../../controllers/v1/types"; const cohere = new CohereClient({ - token: process.env.COHERE_API_KEY + token: process.env.COHERE_API_KEY, }); export async function rerankDocuments( documents: (string | Record)[], query: string, topN = 3, - model = "rerank-english-v3.0" + model = "rerank-english-v3.0", ) { const rerank = await cohere.v2.rerank({ documents, query, topN, model, - returnDocuments: true + returnDocuments: true, }); return rerank.results @@ -23,6 +23,6 @@ export async function rerankDocuments( .map((x) => ({ document: x.document, index: x.index, - relevanceScore: x.relevanceScore + relevanceScore: x.relevanceScore, })); } diff --git a/apps/api/src/lib/html-to-markdown.ts b/apps/api/src/lib/html-to-markdown.ts index 7a0020d1..cba1a80b 100644 --- a/apps/api/src/lib/html-to-markdown.ts +++ b/apps/api/src/lib/html-to-markdown.ts @@ -13,7 +13,7 @@ const goExecutablePath = join( process.cwd(), "sharedLibs", "go-html-to-md", - "html-to-markdown.so" + "html-to-markdown.so", ); class GoMarkdownConverter { @@ -51,7 +51,7 @@ class GoMarkdownConverter { } export async function parseMarkdown( - html: string | null | undefined + html: string | null | undefined, ): Promise { if (!html) { return ""; @@ -74,12 +74,12 @@ export async function parseMarkdown( ) { Sentry.captureException(error); logger.error( - `Error converting HTML to Markdown with Go parser: ${error}` + `Error converting HTML to Markdown with Go parser: ${error}`, ); } else { logger.warn( "Tried to use Go parser, but it doesn't exist in the file system.", - { goExecutablePath } + { goExecutablePath }, ); } } @@ -101,7 +101,7 @@ export async function parseMarkdown( var href = node.getAttribute("href").trim(); var title = node.title ? ' "' + node.title + '"' : ""; return "[" + content.trim() + "](" + href + title + ")\n"; - } + }, }); var gfm = turndownPluginGfm.gfm; turndownService.use(gfm); @@ -145,7 +145,7 @@ function removeSkipToContentLinks(markdownContent: string): string { // Remove [Skip to Content](#page) and [Skip to content](#skip) const newMarkdownContent = markdownContent.replace( /\[Skip to Content\]\(#[^\)]*\)/gi, - "" + "", ); return newMarkdownContent; } diff --git a/apps/api/src/lib/job-priority.ts b/apps/api/src/lib/job-priority.ts index 2bafc3e6..7e2d44de 100644 --- a/apps/api/src/lib/job-priority.ts +++ b/apps/api/src/lib/job-priority.ts @@ -31,7 +31,7 @@ export async function deleteJobPriority(team_id, job_id) { export async function getJobPriority({ plan, team_id, - basePriority = 10 + basePriority = 10, }: { plan: PlanType | undefined; team_id: string; @@ -91,12 +91,12 @@ export async function getJobPriority({ } else { // If not, we keep base priority + planModifier return Math.ceil( - basePriority + Math.ceil((setLength - bucketLimit) * planModifier) + basePriority + Math.ceil((setLength - bucketLimit) * planModifier), ); } } catch (e) { logger.error( - `Get job priority failed: ${team_id}, ${plan}, ${basePriority}` + `Get job priority failed: ${team_id}, ${plan}, ${basePriority}`, ); return basePriority; } diff --git a/apps/api/src/lib/logger.ts b/apps/api/src/lib/logger.ts index 6996ffd4..3cc04a11 100644 --- a/apps/api/src/lib/logger.ts +++ b/apps/api/src/lib/logger.ts @@ -14,14 +14,14 @@ const logFormat = winston.format.printf( name: value.name, message: value.message, stack: value.stack, - cause: value.cause + cause: value.cause, }; } else { return value; } }) : "" - }` + }`, ); export const logger = winston.createLogger({ @@ -34,26 +34,26 @@ export const logger = winston.createLogger({ name: value.name, message: value.message, stack: value.stack, - cause: value.cause + cause: value.cause, }; } else { return value; } - } + }, }), transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.metadata({ - fillExcept: ["message", "level", "timestamp"] + fillExcept: ["message", "level", "timestamp"], }), ...((process.env.ENV === "production" && process.env.SENTRY_ENVIRONMENT === "dev") || process.env.ENV !== "production" ? [winston.format.colorize(), logFormat] - : []) - ) - }) - ] + : []), + ), + }), + ], }); diff --git a/apps/api/src/lib/map-cosine.ts b/apps/api/src/lib/map-cosine.ts index 2a089548..a6c06e27 100644 --- a/apps/api/src/lib/map-cosine.ts +++ b/apps/api/src/lib/map-cosine.ts @@ -6,10 +6,10 @@ export function performCosineSimilarity(links: string[], searchQuery: string) { const cosineSimilarity = (vec1: number[], vec2: number[]): number => { const dotProduct = vec1.reduce((sum, val, i) => sum + val * vec2[i], 0); const magnitude1 = Math.sqrt( - vec1.reduce((sum, val) => sum + val * val, 0) + vec1.reduce((sum, val) => sum + val * val, 0), ); const magnitude2 = Math.sqrt( - vec2.reduce((sum, val) => sum + val * val, 0) + vec2.reduce((sum, val) => sum + val * val, 0), ); if (magnitude1 === 0 || magnitude2 === 0) return 0; return dotProduct / (magnitude1 * magnitude2); diff --git a/apps/api/src/lib/ranker.test.ts b/apps/api/src/lib/ranker.test.ts index 2b30de19..b884c2fb 100644 --- a/apps/api/src/lib/ranker.test.ts +++ b/apps/api/src/lib/ranker.test.ts @@ -5,13 +5,13 @@ describe("performRanking", () => { const linksWithContext = [ "url: https://example.com/dogs, title: All about dogs, description: Learn about different dog breeds", "url: https://example.com/cats, title: Cat care guide, description: Everything about cats", - "url: https://example.com/pets, title: General pet care, description: Care for all types of pets" + "url: https://example.com/pets, title: General pet care, description: Care for all types of pets", ]; const links = [ "https://example.com/dogs", "https://example.com/cats", - "https://example.com/pets" + "https://example.com/pets", ]; const searchQuery = "cats training"; @@ -50,7 +50,7 @@ describe("performRanking", () => { it("should maintain original order for equal scores", async () => { const linksWithContext = [ "url: https://example.com/1, title: Similar content A, description: test", - "url: https://example.com/2, title: Similar content B, description: test" + "url: https://example.com/2, title: Similar content B, description: test", ]; const links = ["https://example.com/1", "https://example.com/2"]; diff --git a/apps/api/src/lib/ranker.ts b/apps/api/src/lib/ranker.ts index 2f06d76d..bffbc9c2 100644 --- a/apps/api/src/lib/ranker.ts +++ b/apps/api/src/lib/ranker.ts @@ -5,14 +5,14 @@ import OpenAI from "openai"; configDotenv(); const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY + apiKey: process.env.OPENAI_API_KEY, }); async function getEmbedding(text: string) { const embedding = await openai.embeddings.create({ model: "text-embedding-ada-002", input: text, - encoding_format: "float" + encoding_format: "float", }); return embedding.data[0].embedding; @@ -39,7 +39,7 @@ const textToVector = (searchQuery: string, text: string): number[] => { async function performRanking( linksWithContext: string[], links: string[], - searchQuery: string + searchQuery: string, ) { try { // Handle invalid inputs @@ -64,7 +64,7 @@ async function performRanking( link: links[index], linkWithContext, score, - originalIndex: index + originalIndex: index, }; } catch (err) { // If embedding fails for a link, return with score 0 @@ -72,10 +72,10 @@ async function performRanking( link: links[index], linkWithContext, score: 0, - originalIndex: index + originalIndex: index, }; } - }) + }), ); // Sort links based on similarity scores while preserving original order for equal scores diff --git a/apps/api/src/lib/scrape-events.ts b/apps/api/src/lib/scrape-events.ts index 6c39c722..97e2cecc 100644 --- a/apps/api/src/lib/scrape-events.ts +++ b/apps/api/src/lib/scrape-events.ts @@ -56,7 +56,7 @@ export class ScrapeEvents { .insert({ job_id: jobId, type: content.type, - content: content + content: content, // created_at }) .select() @@ -73,7 +73,7 @@ export class ScrapeEvents { static async updateScrapeResult( logId: number | null, - result: ScrapeScrapeEvent["result"] + result: ScrapeScrapeEvent["result"], ) { if (logId === null) return; @@ -86,8 +86,8 @@ export class ScrapeEvents { .update({ content: { ...previousLog.content, - result - } + result, + }, }) .eq("id", logId); } catch (error) { @@ -100,7 +100,7 @@ export class ScrapeEvents { await this.insert(((job as any).id ? (job as any).id : job) as string, { type: "queue", event, - worker: process.env.FLY_MACHINE_ID + worker: process.env.FLY_MACHINE_ID, }); } catch (error) { logger.error(`Error logging job event: ${error}`); diff --git a/apps/api/src/lib/validate-country.ts b/apps/api/src/lib/validate-country.ts index 797ea542..bff1c25c 100644 --- a/apps/api/src/lib/validate-country.ts +++ b/apps/api/src/lib/validate-country.ts @@ -6,7 +6,7 @@ export const countries = { continent: "EU", capital: "Andorra la Vella", currency: ["EUR"], - languages: ["ca"] + languages: ["ca"], }, AE: { name: "United Arab Emirates", @@ -15,7 +15,7 @@ export const countries = { continent: "AS", capital: "Abu Dhabi", currency: ["AED"], - languages: ["ar"] + languages: ["ar"], }, AF: { name: "Afghanistan", @@ -24,7 +24,7 @@ export const countries = { continent: "AS", capital: "Kabul", currency: ["AFN"], - languages: ["ps", "uz", "tk"] + languages: ["ps", "uz", "tk"], }, AG: { name: "Antigua and Barbuda", @@ -33,7 +33,7 @@ export const countries = { continent: "NA", capital: "Saint John's", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, AI: { name: "Anguilla", @@ -42,7 +42,7 @@ export const countries = { continent: "NA", capital: "The Valley", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, AL: { name: "Albania", @@ -51,7 +51,7 @@ export const countries = { continent: "EU", capital: "Tirana", currency: ["ALL"], - languages: ["sq"] + languages: ["sq"], }, AM: { name: "Armenia", @@ -60,7 +60,7 @@ export const countries = { continent: "AS", capital: "Yerevan", currency: ["AMD"], - languages: ["hy", "ru"] + languages: ["hy", "ru"], }, AO: { name: "Angola", @@ -69,7 +69,7 @@ export const countries = { continent: "AF", capital: "Luanda", currency: ["AOA"], - languages: ["pt"] + languages: ["pt"], }, AQ: { name: "Antarctica", @@ -78,7 +78,7 @@ export const countries = { continent: "AN", capital: "", currency: [], - languages: [] + languages: [], }, AR: { name: "Argentina", @@ -87,7 +87,7 @@ export const countries = { continent: "SA", capital: "Buenos Aires", currency: ["ARS"], - languages: ["es", "gn"] + languages: ["es", "gn"], }, AS: { name: "American Samoa", @@ -96,7 +96,7 @@ export const countries = { continent: "OC", capital: "Pago Pago", currency: ["USD"], - languages: ["en", "sm"] + languages: ["en", "sm"], }, AT: { name: "Austria", @@ -105,7 +105,7 @@ export const countries = { continent: "EU", capital: "Vienna", currency: ["EUR"], - languages: ["de"] + languages: ["de"], }, AU: { name: "Australia", @@ -114,7 +114,7 @@ export const countries = { continent: "OC", capital: "Canberra", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, AW: { name: "Aruba", @@ -123,7 +123,7 @@ export const countries = { continent: "NA", capital: "Oranjestad", currency: ["AWG"], - languages: ["nl", "pa"] + languages: ["nl", "pa"], }, AX: { name: "Aland", @@ -133,7 +133,7 @@ export const countries = { capital: "Mariehamn", currency: ["EUR"], languages: ["sv"], - partOf: "FI" + partOf: "FI", }, AZ: { name: "Azerbaijan", @@ -143,7 +143,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Baku", currency: ["AZN"], - languages: ["az"] + languages: ["az"], }, BA: { name: "Bosnia and Herzegovina", @@ -152,7 +152,7 @@ export const countries = { continent: "EU", capital: "Sarajevo", currency: ["BAM"], - languages: ["bs", "hr", "sr"] + languages: ["bs", "hr", "sr"], }, BB: { name: "Barbados", @@ -161,7 +161,7 @@ export const countries = { continent: "NA", capital: "Bridgetown", currency: ["BBD"], - languages: ["en"] + languages: ["en"], }, BD: { name: "Bangladesh", @@ -170,7 +170,7 @@ export const countries = { continent: "AS", capital: "Dhaka", currency: ["BDT"], - languages: ["bn"] + languages: ["bn"], }, BE: { name: "Belgium", @@ -179,7 +179,7 @@ export const countries = { continent: "EU", capital: "Brussels", currency: ["EUR"], - languages: ["nl", "fr", "de"] + languages: ["nl", "fr", "de"], }, BF: { name: "Burkina Faso", @@ -188,7 +188,7 @@ export const countries = { continent: "AF", capital: "Ouagadougou", currency: ["XOF"], - languages: ["fr", "ff"] + languages: ["fr", "ff"], }, BG: { name: "Bulgaria", @@ -197,7 +197,7 @@ export const countries = { continent: "EU", capital: "Sofia", currency: ["BGN"], - languages: ["bg"] + languages: ["bg"], }, BH: { name: "Bahrain", @@ -206,7 +206,7 @@ export const countries = { continent: "AS", capital: "Manama", currency: ["BHD"], - languages: ["ar"] + languages: ["ar"], }, BI: { name: "Burundi", @@ -215,7 +215,7 @@ export const countries = { continent: "AF", capital: "Bujumbura", currency: ["BIF"], - languages: ["fr", "rn"] + languages: ["fr", "rn"], }, BJ: { name: "Benin", @@ -224,7 +224,7 @@ export const countries = { continent: "AF", capital: "Porto-Novo", currency: ["XOF"], - languages: ["fr"] + languages: ["fr"], }, BL: { name: "Saint Barthelemy", @@ -233,7 +233,7 @@ export const countries = { continent: "NA", capital: "Gustavia", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, BM: { name: "Bermuda", @@ -242,7 +242,7 @@ export const countries = { continent: "NA", capital: "Hamilton", currency: ["BMD"], - languages: ["en"] + languages: ["en"], }, BN: { name: "Brunei", @@ -251,7 +251,7 @@ export const countries = { continent: "AS", capital: "Bandar Seri Begawan", currency: ["BND"], - languages: ["ms"] + languages: ["ms"], }, BO: { name: "Bolivia", @@ -260,7 +260,7 @@ export const countries = { continent: "SA", capital: "Sucre", currency: ["BOB", "BOV"], - languages: ["es", "ay", "qu"] + languages: ["es", "ay", "qu"], }, BQ: { name: "Bonaire", @@ -269,7 +269,7 @@ export const countries = { continent: "NA", capital: "Kralendijk", currency: ["USD"], - languages: ["nl"] + languages: ["nl"], }, BR: { name: "Brazil", @@ -278,7 +278,7 @@ export const countries = { continent: "SA", capital: "Brasília", currency: ["BRL"], - languages: ["pt"] + languages: ["pt"], }, BS: { name: "Bahamas", @@ -287,7 +287,7 @@ export const countries = { continent: "NA", capital: "Nassau", currency: ["BSD"], - languages: ["en"] + languages: ["en"], }, BT: { name: "Bhutan", @@ -296,7 +296,7 @@ export const countries = { continent: "AS", capital: "Thimphu", currency: ["BTN", "INR"], - languages: ["dz"] + languages: ["dz"], }, BV: { name: "Bouvet Island", @@ -305,7 +305,7 @@ export const countries = { continent: "AN", capital: "", currency: ["NOK"], - languages: ["no", "nb", "nn"] + languages: ["no", "nb", "nn"], }, BW: { name: "Botswana", @@ -314,7 +314,7 @@ export const countries = { continent: "AF", capital: "Gaborone", currency: ["BWP"], - languages: ["en", "tn"] + languages: ["en", "tn"], }, BY: { name: "Belarus", @@ -323,7 +323,7 @@ export const countries = { continent: "EU", capital: "Minsk", currency: ["BYN"], - languages: ["be", "ru"] + languages: ["be", "ru"], }, BZ: { name: "Belize", @@ -332,7 +332,7 @@ export const countries = { continent: "NA", capital: "Belmopan", currency: ["BZD"], - languages: ["en", "es"] + languages: ["en", "es"], }, CA: { name: "Canada", @@ -341,7 +341,7 @@ export const countries = { continent: "NA", capital: "Ottawa", currency: ["CAD"], - languages: ["en", "fr"] + languages: ["en", "fr"], }, CC: { name: "Cocos (Keeling) Islands", @@ -350,7 +350,7 @@ export const countries = { continent: "AS", capital: "West Island", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, CD: { name: "Democratic Republic of the Congo", @@ -359,7 +359,7 @@ export const countries = { continent: "AF", capital: "Kinshasa", currency: ["CDF"], - languages: ["fr", "ln", "kg", "sw", "lu"] + languages: ["fr", "ln", "kg", "sw", "lu"], }, CF: { name: "Central African Republic", @@ -368,7 +368,7 @@ export const countries = { continent: "AF", capital: "Bangui", currency: ["XAF"], - languages: ["fr", "sg"] + languages: ["fr", "sg"], }, CG: { name: "Republic of the Congo", @@ -377,7 +377,7 @@ export const countries = { continent: "AF", capital: "Brazzaville", currency: ["XAF"], - languages: ["fr", "ln"] + languages: ["fr", "ln"], }, CH: { name: "Switzerland", @@ -386,7 +386,7 @@ export const countries = { continent: "EU", capital: "Bern", currency: ["CHE", "CHF", "CHW"], - languages: ["de", "fr", "it"] + languages: ["de", "fr", "it"], }, CI: { name: "Ivory Coast", @@ -395,7 +395,7 @@ export const countries = { continent: "AF", capital: "Yamoussoukro", currency: ["XOF"], - languages: ["fr"] + languages: ["fr"], }, CK: { name: "Cook Islands", @@ -404,7 +404,7 @@ export const countries = { continent: "OC", capital: "Avarua", currency: ["NZD"], - languages: ["en"] + languages: ["en"], }, CL: { name: "Chile", @@ -413,7 +413,7 @@ export const countries = { continent: "SA", capital: "Santiago", currency: ["CLF", "CLP"], - languages: ["es"] + languages: ["es"], }, CM: { name: "Cameroon", @@ -422,7 +422,7 @@ export const countries = { continent: "AF", capital: "Yaoundé", currency: ["XAF"], - languages: ["en", "fr"] + languages: ["en", "fr"], }, CN: { name: "China", @@ -431,7 +431,7 @@ export const countries = { continent: "AS", capital: "Beijing", currency: ["CNY"], - languages: ["zh"] + languages: ["zh"], }, CO: { name: "Colombia", @@ -440,7 +440,7 @@ export const countries = { continent: "SA", capital: "Bogotá", currency: ["COP"], - languages: ["es"] + languages: ["es"], }, CR: { name: "Costa Rica", @@ -449,7 +449,7 @@ export const countries = { continent: "NA", capital: "San José", currency: ["CRC"], - languages: ["es"] + languages: ["es"], }, CU: { name: "Cuba", @@ -458,7 +458,7 @@ export const countries = { continent: "NA", capital: "Havana", currency: ["CUC", "CUP"], - languages: ["es"] + languages: ["es"], }, CV: { name: "Cape Verde", @@ -467,7 +467,7 @@ export const countries = { continent: "AF", capital: "Praia", currency: ["CVE"], - languages: ["pt"] + languages: ["pt"], }, CW: { name: "Curacao", @@ -476,7 +476,7 @@ export const countries = { continent: "NA", capital: "Willemstad", currency: ["ANG"], - languages: ["nl", "pa", "en"] + languages: ["nl", "pa", "en"], }, CX: { name: "Christmas Island", @@ -485,7 +485,7 @@ export const countries = { continent: "AS", capital: "Flying Fish Cove", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, CY: { name: "Cyprus", @@ -494,7 +494,7 @@ export const countries = { continent: "EU", capital: "Nicosia", currency: ["EUR"], - languages: ["el", "tr", "hy"] + languages: ["el", "tr", "hy"], }, CZ: { name: "Czech Republic", @@ -503,7 +503,7 @@ export const countries = { continent: "EU", capital: "Prague", currency: ["CZK"], - languages: ["cs"] + languages: ["cs"], }, DE: { name: "Germany", @@ -512,7 +512,7 @@ export const countries = { continent: "EU", capital: "Berlin", currency: ["EUR"], - languages: ["de"] + languages: ["de"], }, DJ: { name: "Djibouti", @@ -521,7 +521,7 @@ export const countries = { continent: "AF", capital: "Djibouti", currency: ["DJF"], - languages: ["fr", "ar"] + languages: ["fr", "ar"], }, DK: { name: "Denmark", @@ -531,7 +531,7 @@ export const countries = { continents: ["EU", "NA"], capital: "Copenhagen", currency: ["DKK"], - languages: ["da"] + languages: ["da"], }, DM: { name: "Dominica", @@ -540,7 +540,7 @@ export const countries = { continent: "NA", capital: "Roseau", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, DO: { name: "Dominican Republic", @@ -549,7 +549,7 @@ export const countries = { continent: "NA", capital: "Santo Domingo", currency: ["DOP"], - languages: ["es"] + languages: ["es"], }, DZ: { name: "Algeria", @@ -558,7 +558,7 @@ export const countries = { continent: "AF", capital: "Algiers", currency: ["DZD"], - languages: ["ar"] + languages: ["ar"], }, EC: { name: "Ecuador", @@ -567,7 +567,7 @@ export const countries = { continent: "SA", capital: "Quito", currency: ["USD"], - languages: ["es"] + languages: ["es"], }, EE: { name: "Estonia", @@ -576,7 +576,7 @@ export const countries = { continent: "EU", capital: "Tallinn", currency: ["EUR"], - languages: ["et"] + languages: ["et"], }, EG: { name: "Egypt", @@ -586,7 +586,7 @@ export const countries = { continents: ["AF", "AS"], capital: "Cairo", currency: ["EGP"], - languages: ["ar"] + languages: ["ar"], }, EH: { name: "Western Sahara", @@ -595,7 +595,7 @@ export const countries = { continent: "AF", capital: "El Aaiún", currency: ["MAD", "DZD", "MRU"], - languages: ["es"] + languages: ["es"], }, ER: { name: "Eritrea", @@ -604,7 +604,7 @@ export const countries = { continent: "AF", capital: "Asmara", currency: ["ERN"], - languages: ["ti", "ar", "en"] + languages: ["ti", "ar", "en"], }, ES: { name: "Spain", @@ -613,7 +613,7 @@ export const countries = { continent: "EU", capital: "Madrid", currency: ["EUR"], - languages: ["es", "eu", "ca", "gl", "oc"] + languages: ["es", "eu", "ca", "gl", "oc"], }, ET: { name: "Ethiopia", @@ -622,7 +622,7 @@ export const countries = { continent: "AF", capital: "Addis Ababa", currency: ["ETB"], - languages: ["am"] + languages: ["am"], }, FI: { name: "Finland", @@ -631,7 +631,7 @@ export const countries = { continent: "EU", capital: "Helsinki", currency: ["EUR"], - languages: ["fi", "sv"] + languages: ["fi", "sv"], }, FJ: { name: "Fiji", @@ -640,7 +640,7 @@ export const countries = { continent: "OC", capital: "Suva", currency: ["FJD"], - languages: ["en", "fj", "hi", "ur"] + languages: ["en", "fj", "hi", "ur"], }, FK: { name: "Falkland Islands", @@ -649,7 +649,7 @@ export const countries = { continent: "SA", capital: "Stanley", currency: ["FKP"], - languages: ["en"] + languages: ["en"], }, FM: { name: "Micronesia", @@ -658,7 +658,7 @@ export const countries = { continent: "OC", capital: "Palikir", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, FO: { name: "Faroe Islands", @@ -667,7 +667,7 @@ export const countries = { continent: "EU", capital: "Tórshavn", currency: ["DKK"], - languages: ["fo"] + languages: ["fo"], }, FR: { name: "France", @@ -676,7 +676,7 @@ export const countries = { continent: "EU", capital: "Paris", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, GA: { name: "Gabon", @@ -685,7 +685,7 @@ export const countries = { continent: "AF", capital: "Libreville", currency: ["XAF"], - languages: ["fr"] + languages: ["fr"], }, GB: { name: "United Kingdom", @@ -694,7 +694,7 @@ export const countries = { continent: "EU", capital: "London", currency: ["GBP"], - languages: ["en"] + languages: ["en"], }, GD: { name: "Grenada", @@ -703,7 +703,7 @@ export const countries = { continent: "NA", capital: "St. George's", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, GE: { name: "Georgia", @@ -713,7 +713,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Tbilisi", currency: ["GEL"], - languages: ["ka"] + languages: ["ka"], }, GF: { name: "French Guiana", @@ -722,7 +722,7 @@ export const countries = { continent: "SA", capital: "Cayenne", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, GG: { name: "Guernsey", @@ -731,7 +731,7 @@ export const countries = { continent: "EU", capital: "St. Peter Port", currency: ["GBP"], - languages: ["en", "fr"] + languages: ["en", "fr"], }, GH: { name: "Ghana", @@ -740,7 +740,7 @@ export const countries = { continent: "AF", capital: "Accra", currency: ["GHS"], - languages: ["en"] + languages: ["en"], }, GI: { name: "Gibraltar", @@ -749,7 +749,7 @@ export const countries = { continent: "EU", capital: "Gibraltar", currency: ["GIP"], - languages: ["en"] + languages: ["en"], }, GL: { name: "Greenland", @@ -758,7 +758,7 @@ export const countries = { continent: "NA", capital: "Nuuk", currency: ["DKK"], - languages: ["kl"] + languages: ["kl"], }, GM: { name: "Gambia", @@ -767,7 +767,7 @@ export const countries = { continent: "AF", capital: "Banjul", currency: ["GMD"], - languages: ["en"] + languages: ["en"], }, GN: { name: "Guinea", @@ -776,7 +776,7 @@ export const countries = { continent: "AF", capital: "Conakry", currency: ["GNF"], - languages: ["fr", "ff"] + languages: ["fr", "ff"], }, GP: { name: "Guadeloupe", @@ -785,7 +785,7 @@ export const countries = { continent: "NA", capital: "Basse-Terre", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, GQ: { name: "Equatorial Guinea", @@ -794,7 +794,7 @@ export const countries = { continent: "AF", capital: "Malabo", currency: ["XAF"], - languages: ["es", "fr"] + languages: ["es", "fr"], }, GR: { name: "Greece", @@ -803,7 +803,7 @@ export const countries = { continent: "EU", capital: "Athens", currency: ["EUR"], - languages: ["el"] + languages: ["el"], }, GS: { name: "South Georgia and the South Sandwich Islands", @@ -812,7 +812,7 @@ export const countries = { continent: "AN", capital: "King Edward Point", currency: ["GBP"], - languages: ["en"] + languages: ["en"], }, GT: { name: "Guatemala", @@ -821,7 +821,7 @@ export const countries = { continent: "NA", capital: "Guatemala City", currency: ["GTQ"], - languages: ["es"] + languages: ["es"], }, GU: { name: "Guam", @@ -830,7 +830,7 @@ export const countries = { continent: "OC", capital: "Hagåtña", currency: ["USD"], - languages: ["en", "ch", "es"] + languages: ["en", "ch", "es"], }, GW: { name: "Guinea-Bissau", @@ -839,7 +839,7 @@ export const countries = { continent: "AF", capital: "Bissau", currency: ["XOF"], - languages: ["pt"] + languages: ["pt"], }, GY: { name: "Guyana", @@ -848,7 +848,7 @@ export const countries = { continent: "SA", capital: "Georgetown", currency: ["GYD"], - languages: ["en"] + languages: ["en"], }, HK: { name: "Hong Kong", @@ -857,7 +857,7 @@ export const countries = { continent: "AS", capital: "City of Victoria", currency: ["HKD"], - languages: ["zh", "en"] + languages: ["zh", "en"], }, HM: { name: "Heard Island and McDonald Islands", @@ -866,7 +866,7 @@ export const countries = { continent: "AN", capital: "", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, HN: { name: "Honduras", @@ -875,7 +875,7 @@ export const countries = { continent: "NA", capital: "Tegucigalpa", currency: ["HNL"], - languages: ["es"] + languages: ["es"], }, HR: { name: "Croatia", @@ -884,7 +884,7 @@ export const countries = { continent: "EU", capital: "Zagreb", currency: ["EUR"], - languages: ["hr"] + languages: ["hr"], }, HT: { name: "Haiti", @@ -893,7 +893,7 @@ export const countries = { continent: "NA", capital: "Port-au-Prince", currency: ["HTG", "USD"], - languages: ["fr", "ht"] + languages: ["fr", "ht"], }, HU: { name: "Hungary", @@ -902,7 +902,7 @@ export const countries = { continent: "EU", capital: "Budapest", currency: ["HUF"], - languages: ["hu"] + languages: ["hu"], }, ID: { name: "Indonesia", @@ -911,7 +911,7 @@ export const countries = { continent: "AS", capital: "Jakarta", currency: ["IDR"], - languages: ["id"] + languages: ["id"], }, IE: { name: "Ireland", @@ -920,7 +920,7 @@ export const countries = { continent: "EU", capital: "Dublin", currency: ["EUR"], - languages: ["ga", "en"] + languages: ["ga", "en"], }, IL: { name: "Israel", @@ -929,7 +929,7 @@ export const countries = { continent: "AS", capital: "Jerusalem", currency: ["ILS"], - languages: ["he", "ar"] + languages: ["he", "ar"], }, IM: { name: "Isle of Man", @@ -938,7 +938,7 @@ export const countries = { continent: "EU", capital: "Douglas", currency: ["GBP"], - languages: ["en", "gv"] + languages: ["en", "gv"], }, IN: { name: "India", @@ -947,7 +947,7 @@ export const countries = { continent: "AS", capital: "New Delhi", currency: ["INR"], - languages: ["hi", "en"] + languages: ["hi", "en"], }, IO: { name: "British Indian Ocean Territory", @@ -956,7 +956,7 @@ export const countries = { continent: "AS", capital: "Diego Garcia", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, IQ: { name: "Iraq", @@ -965,7 +965,7 @@ export const countries = { continent: "AS", capital: "Baghdad", currency: ["IQD"], - languages: ["ar", "ku"] + languages: ["ar", "ku"], }, IR: { name: "Iran", @@ -974,7 +974,7 @@ export const countries = { continent: "AS", capital: "Tehran", currency: ["IRR"], - languages: ["fa"] + languages: ["fa"], }, IS: { name: "Iceland", @@ -983,7 +983,7 @@ export const countries = { continent: "EU", capital: "Reykjavik", currency: ["ISK"], - languages: ["is"] + languages: ["is"], }, IT: { name: "Italy", @@ -992,7 +992,7 @@ export const countries = { continent: "EU", capital: "Rome", currency: ["EUR"], - languages: ["it"] + languages: ["it"], }, JE: { name: "Jersey", @@ -1001,7 +1001,7 @@ export const countries = { continent: "EU", capital: "Saint Helier", currency: ["GBP"], - languages: ["en", "fr"] + languages: ["en", "fr"], }, JM: { name: "Jamaica", @@ -1010,7 +1010,7 @@ export const countries = { continent: "NA", capital: "Kingston", currency: ["JMD"], - languages: ["en"] + languages: ["en"], }, JO: { name: "Jordan", @@ -1019,7 +1019,7 @@ export const countries = { continent: "AS", capital: "Amman", currency: ["JOD"], - languages: ["ar"] + languages: ["ar"], }, JP: { name: "Japan", @@ -1028,7 +1028,7 @@ export const countries = { continent: "AS", capital: "Tokyo", currency: ["JPY"], - languages: ["ja"] + languages: ["ja"], }, KE: { name: "Kenya", @@ -1037,7 +1037,7 @@ export const countries = { continent: "AF", capital: "Nairobi", currency: ["KES"], - languages: ["en", "sw"] + languages: ["en", "sw"], }, KG: { name: "Kyrgyzstan", @@ -1046,7 +1046,7 @@ export const countries = { continent: "AS", capital: "Bishkek", currency: ["KGS"], - languages: ["ky", "ru"] + languages: ["ky", "ru"], }, KH: { name: "Cambodia", @@ -1055,7 +1055,7 @@ export const countries = { continent: "AS", capital: "Phnom Penh", currency: ["KHR"], - languages: ["km"] + languages: ["km"], }, KI: { name: "Kiribati", @@ -1064,7 +1064,7 @@ export const countries = { continent: "OC", capital: "South Tarawa", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, KM: { name: "Comoros", @@ -1073,7 +1073,7 @@ export const countries = { continent: "AF", capital: "Moroni", currency: ["KMF"], - languages: ["ar", "fr"] + languages: ["ar", "fr"], }, KN: { name: "Saint Kitts and Nevis", @@ -1082,7 +1082,7 @@ export const countries = { continent: "NA", capital: "Basseterre", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, KP: { name: "North Korea", @@ -1091,7 +1091,7 @@ export const countries = { continent: "AS", capital: "Pyongyang", currency: ["KPW"], - languages: ["ko"] + languages: ["ko"], }, KR: { name: "South Korea", @@ -1100,7 +1100,7 @@ export const countries = { continent: "AS", capital: "Seoul", currency: ["KRW"], - languages: ["ko"] + languages: ["ko"], }, KW: { name: "Kuwait", @@ -1109,7 +1109,7 @@ export const countries = { continent: "AS", capital: "Kuwait City", currency: ["KWD"], - languages: ["ar"] + languages: ["ar"], }, KY: { name: "Cayman Islands", @@ -1118,7 +1118,7 @@ export const countries = { continent: "NA", capital: "George Town", currency: ["KYD"], - languages: ["en"] + languages: ["en"], }, KZ: { name: "Kazakhstan", @@ -1128,7 +1128,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Astana", currency: ["KZT"], - languages: ["kk", "ru"] + languages: ["kk", "ru"], }, LA: { name: "Laos", @@ -1137,7 +1137,7 @@ export const countries = { continent: "AS", capital: "Vientiane", currency: ["LAK"], - languages: ["lo"] + languages: ["lo"], }, LB: { name: "Lebanon", @@ -1146,7 +1146,7 @@ export const countries = { continent: "AS", capital: "Beirut", currency: ["LBP"], - languages: ["ar", "fr"] + languages: ["ar", "fr"], }, LC: { name: "Saint Lucia", @@ -1155,7 +1155,7 @@ export const countries = { continent: "NA", capital: "Castries", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, LI: { name: "Liechtenstein", @@ -1164,7 +1164,7 @@ export const countries = { continent: "EU", capital: "Vaduz", currency: ["CHF"], - languages: ["de"] + languages: ["de"], }, LK: { name: "Sri Lanka", @@ -1173,7 +1173,7 @@ export const countries = { continent: "AS", capital: "Colombo", currency: ["LKR"], - languages: ["si", "ta"] + languages: ["si", "ta"], }, LR: { name: "Liberia", @@ -1182,7 +1182,7 @@ export const countries = { continent: "AF", capital: "Monrovia", currency: ["LRD"], - languages: ["en"] + languages: ["en"], }, LS: { name: "Lesotho", @@ -1191,7 +1191,7 @@ export const countries = { continent: "AF", capital: "Maseru", currency: ["LSL", "ZAR"], - languages: ["en", "st"] + languages: ["en", "st"], }, LT: { name: "Lithuania", @@ -1200,7 +1200,7 @@ export const countries = { continent: "EU", capital: "Vilnius", currency: ["EUR"], - languages: ["lt"] + languages: ["lt"], }, LU: { name: "Luxembourg", @@ -1209,7 +1209,7 @@ export const countries = { continent: "EU", capital: "Luxembourg", currency: ["EUR"], - languages: ["fr", "de", "lb"] + languages: ["fr", "de", "lb"], }, LV: { name: "Latvia", @@ -1218,7 +1218,7 @@ export const countries = { continent: "EU", capital: "Riga", currency: ["EUR"], - languages: ["lv"] + languages: ["lv"], }, LY: { name: "Libya", @@ -1227,7 +1227,7 @@ export const countries = { continent: "AF", capital: "Tripoli", currency: ["LYD"], - languages: ["ar"] + languages: ["ar"], }, MA: { name: "Morocco", @@ -1236,7 +1236,7 @@ export const countries = { continent: "AF", capital: "Rabat", currency: ["MAD"], - languages: ["ar"] + languages: ["ar"], }, MC: { name: "Monaco", @@ -1245,7 +1245,7 @@ export const countries = { continent: "EU", capital: "Monaco", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, MD: { name: "Moldova", @@ -1254,7 +1254,7 @@ export const countries = { continent: "EU", capital: "Chișinău", currency: ["MDL"], - languages: ["ro"] + languages: ["ro"], }, ME: { name: "Montenegro", @@ -1263,7 +1263,7 @@ export const countries = { continent: "EU", capital: "Podgorica", currency: ["EUR"], - languages: ["sr", "bs", "sq", "hr"] + languages: ["sr", "bs", "sq", "hr"], }, MF: { name: "Saint Martin", @@ -1272,7 +1272,7 @@ export const countries = { continent: "NA", capital: "Marigot", currency: ["EUR"], - languages: ["en", "fr", "nl"] + languages: ["en", "fr", "nl"], }, MG: { name: "Madagascar", @@ -1281,7 +1281,7 @@ export const countries = { continent: "AF", capital: "Antananarivo", currency: ["MGA"], - languages: ["fr", "mg"] + languages: ["fr", "mg"], }, MH: { name: "Marshall Islands", @@ -1290,7 +1290,7 @@ export const countries = { continent: "OC", capital: "Majuro", currency: ["USD"], - languages: ["en", "mh"] + languages: ["en", "mh"], }, MK: { name: "North Macedonia", @@ -1299,7 +1299,7 @@ export const countries = { continent: "EU", capital: "Skopje", currency: ["MKD"], - languages: ["mk"] + languages: ["mk"], }, ML: { name: "Mali", @@ -1308,7 +1308,7 @@ export const countries = { continent: "AF", capital: "Bamako", currency: ["XOF"], - languages: ["fr"] + languages: ["fr"], }, MM: { name: "Myanmar (Burma)", @@ -1317,7 +1317,7 @@ export const countries = { continent: "AS", capital: "Naypyidaw", currency: ["MMK"], - languages: ["my"] + languages: ["my"], }, MN: { name: "Mongolia", @@ -1326,7 +1326,7 @@ export const countries = { continent: "AS", capital: "Ulan Bator", currency: ["MNT"], - languages: ["mn"] + languages: ["mn"], }, MO: { name: "Macao", @@ -1335,7 +1335,7 @@ export const countries = { continent: "AS", capital: "", currency: ["MOP"], - languages: ["zh", "pt"] + languages: ["zh", "pt"], }, MP: { name: "Northern Mariana Islands", @@ -1344,7 +1344,7 @@ export const countries = { continent: "OC", capital: "Saipan", currency: ["USD"], - languages: ["en", "ch"] + languages: ["en", "ch"], }, MQ: { name: "Martinique", @@ -1353,7 +1353,7 @@ export const countries = { continent: "NA", capital: "Fort-de-France", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, MR: { name: "Mauritania", @@ -1362,7 +1362,7 @@ export const countries = { continent: "AF", capital: "Nouakchott", currency: ["MRU"], - languages: ["ar"] + languages: ["ar"], }, MS: { name: "Montserrat", @@ -1371,7 +1371,7 @@ export const countries = { continent: "NA", capital: "Plymouth", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, MT: { name: "Malta", @@ -1380,7 +1380,7 @@ export const countries = { continent: "EU", capital: "Valletta", currency: ["EUR"], - languages: ["mt", "en"] + languages: ["mt", "en"], }, MU: { name: "Mauritius", @@ -1389,7 +1389,7 @@ export const countries = { continent: "AF", capital: "Port Louis", currency: ["MUR"], - languages: ["en"] + languages: ["en"], }, MV: { name: "Maldives", @@ -1398,7 +1398,7 @@ export const countries = { continent: "AS", capital: "Malé", currency: ["MVR"], - languages: ["dv"] + languages: ["dv"], }, MW: { name: "Malawi", @@ -1407,7 +1407,7 @@ export const countries = { continent: "AF", capital: "Lilongwe", currency: ["MWK"], - languages: ["en", "ny"] + languages: ["en", "ny"], }, MX: { name: "Mexico", @@ -1416,7 +1416,7 @@ export const countries = { continent: "NA", capital: "Mexico City", currency: ["MXN"], - languages: ["es"] + languages: ["es"], }, MY: { name: "Malaysia", @@ -1425,7 +1425,7 @@ export const countries = { continent: "AS", capital: "Kuala Lumpur", currency: ["MYR"], - languages: ["ms"] + languages: ["ms"], }, MZ: { name: "Mozambique", @@ -1434,7 +1434,7 @@ export const countries = { continent: "AF", capital: "Maputo", currency: ["MZN"], - languages: ["pt"] + languages: ["pt"], }, NA: { name: "Namibia", @@ -1443,7 +1443,7 @@ export const countries = { continent: "AF", capital: "Windhoek", currency: ["NAD", "ZAR"], - languages: ["en", "af"] + languages: ["en", "af"], }, NC: { name: "New Caledonia", @@ -1452,7 +1452,7 @@ export const countries = { continent: "OC", capital: "Nouméa", currency: ["XPF"], - languages: ["fr"] + languages: ["fr"], }, NE: { name: "Niger", @@ -1461,7 +1461,7 @@ export const countries = { continent: "AF", capital: "Niamey", currency: ["XOF"], - languages: ["fr"] + languages: ["fr"], }, NF: { name: "Norfolk Island", @@ -1470,7 +1470,7 @@ export const countries = { continent: "OC", capital: "Kingston", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, NG: { name: "Nigeria", @@ -1479,7 +1479,7 @@ export const countries = { continent: "AF", capital: "Abuja", currency: ["NGN"], - languages: ["en"] + languages: ["en"], }, NI: { name: "Nicaragua", @@ -1488,7 +1488,7 @@ export const countries = { continent: "NA", capital: "Managua", currency: ["NIO"], - languages: ["es"] + languages: ["es"], }, NL: { name: "Netherlands", @@ -1497,7 +1497,7 @@ export const countries = { continent: "EU", capital: "Amsterdam", currency: ["EUR"], - languages: ["nl"] + languages: ["nl"], }, NO: { name: "Norway", @@ -1506,7 +1506,7 @@ export const countries = { continent: "EU", capital: "Oslo", currency: ["NOK"], - languages: ["no", "nb", "nn"] + languages: ["no", "nb", "nn"], }, NP: { name: "Nepal", @@ -1515,7 +1515,7 @@ export const countries = { continent: "AS", capital: "Kathmandu", currency: ["NPR"], - languages: ["ne"] + languages: ["ne"], }, NR: { name: "Nauru", @@ -1524,7 +1524,7 @@ export const countries = { continent: "OC", capital: "Yaren", currency: ["AUD"], - languages: ["en", "na"] + languages: ["en", "na"], }, NU: { name: "Niue", @@ -1533,7 +1533,7 @@ export const countries = { continent: "OC", capital: "Alofi", currency: ["NZD"], - languages: ["en"] + languages: ["en"], }, NZ: { name: "New Zealand", @@ -1542,7 +1542,7 @@ export const countries = { continent: "OC", capital: "Wellington", currency: ["NZD"], - languages: ["en", "mi"] + languages: ["en", "mi"], }, OM: { name: "Oman", @@ -1551,7 +1551,7 @@ export const countries = { continent: "AS", capital: "Muscat", currency: ["OMR"], - languages: ["ar"] + languages: ["ar"], }, PA: { name: "Panama", @@ -1560,7 +1560,7 @@ export const countries = { continent: "NA", capital: "Panama City", currency: ["PAB", "USD"], - languages: ["es"] + languages: ["es"], }, PE: { name: "Peru", @@ -1569,7 +1569,7 @@ export const countries = { continent: "SA", capital: "Lima", currency: ["PEN"], - languages: ["es"] + languages: ["es"], }, PF: { name: "French Polynesia", @@ -1578,7 +1578,7 @@ export const countries = { continent: "OC", capital: "Papeetē", currency: ["XPF"], - languages: ["fr"] + languages: ["fr"], }, PG: { name: "Papua New Guinea", @@ -1587,7 +1587,7 @@ export const countries = { continent: "OC", capital: "Port Moresby", currency: ["PGK"], - languages: ["en"] + languages: ["en"], }, PH: { name: "Philippines", @@ -1596,7 +1596,7 @@ export const countries = { continent: "AS", capital: "Manila", currency: ["PHP"], - languages: ["en"] + languages: ["en"], }, PK: { name: "Pakistan", @@ -1605,7 +1605,7 @@ export const countries = { continent: "AS", capital: "Islamabad", currency: ["PKR"], - languages: ["en", "ur"] + languages: ["en", "ur"], }, PL: { name: "Poland", @@ -1614,7 +1614,7 @@ export const countries = { continent: "EU", capital: "Warsaw", currency: ["PLN"], - languages: ["pl"] + languages: ["pl"], }, PM: { name: "Saint Pierre and Miquelon", @@ -1623,7 +1623,7 @@ export const countries = { continent: "NA", capital: "Saint-Pierre", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, PN: { name: "Pitcairn Islands", @@ -1632,7 +1632,7 @@ export const countries = { continent: "OC", capital: "Adamstown", currency: ["NZD"], - languages: ["en"] + languages: ["en"], }, PR: { name: "Puerto Rico", @@ -1641,7 +1641,7 @@ export const countries = { continent: "NA", capital: "San Juan", currency: ["USD"], - languages: ["es", "en"] + languages: ["es", "en"], }, PS: { name: "Palestine", @@ -1650,7 +1650,7 @@ export const countries = { continent: "AS", capital: "Ramallah", currency: ["ILS"], - languages: ["ar"] + languages: ["ar"], }, PT: { name: "Portugal", @@ -1659,7 +1659,7 @@ export const countries = { continent: "EU", capital: "Lisbon", currency: ["EUR"], - languages: ["pt"] + languages: ["pt"], }, PW: { name: "Palau", @@ -1668,7 +1668,7 @@ export const countries = { continent: "OC", capital: "Ngerulmud", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, PY: { name: "Paraguay", @@ -1677,7 +1677,7 @@ export const countries = { continent: "SA", capital: "Asunción", currency: ["PYG"], - languages: ["es", "gn"] + languages: ["es", "gn"], }, QA: { name: "Qatar", @@ -1686,7 +1686,7 @@ export const countries = { continent: "AS", capital: "Doha", currency: ["QAR"], - languages: ["ar"] + languages: ["ar"], }, RE: { name: "Reunion", @@ -1695,7 +1695,7 @@ export const countries = { continent: "AF", capital: "Saint-Denis", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, RO: { name: "Romania", @@ -1704,7 +1704,7 @@ export const countries = { continent: "EU", capital: "Bucharest", currency: ["RON"], - languages: ["ro"] + languages: ["ro"], }, RS: { name: "Serbia", @@ -1713,7 +1713,7 @@ export const countries = { continent: "EU", capital: "Belgrade", currency: ["RSD"], - languages: ["sr"] + languages: ["sr"], }, RU: { name: "Russia", @@ -1723,7 +1723,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Moscow", currency: ["RUB"], - languages: ["ru"] + languages: ["ru"], }, RW: { name: "Rwanda", @@ -1732,7 +1732,7 @@ export const countries = { continent: "AF", capital: "Kigali", currency: ["RWF"], - languages: ["rw", "en", "fr"] + languages: ["rw", "en", "fr"], }, SA: { name: "Saudi Arabia", @@ -1741,7 +1741,7 @@ export const countries = { continent: "AS", capital: "Riyadh", currency: ["SAR"], - languages: ["ar"] + languages: ["ar"], }, SB: { name: "Solomon Islands", @@ -1750,7 +1750,7 @@ export const countries = { continent: "OC", capital: "Honiara", currency: ["SBD"], - languages: ["en"] + languages: ["en"], }, SC: { name: "Seychelles", @@ -1759,7 +1759,7 @@ export const countries = { continent: "AF", capital: "Victoria", currency: ["SCR"], - languages: ["fr", "en"] + languages: ["fr", "en"], }, SD: { name: "Sudan", @@ -1768,7 +1768,7 @@ export const countries = { continent: "AF", capital: "Khartoum", currency: ["SDG"], - languages: ["ar", "en"] + languages: ["ar", "en"], }, SE: { name: "Sweden", @@ -1777,7 +1777,7 @@ export const countries = { continent: "EU", capital: "Stockholm", currency: ["SEK"], - languages: ["sv"] + languages: ["sv"], }, SG: { name: "Singapore", @@ -1786,7 +1786,7 @@ export const countries = { continent: "AS", capital: "Singapore", currency: ["SGD"], - languages: ["en", "ms", "ta", "zh"] + languages: ["en", "ms", "ta", "zh"], }, SH: { name: "Saint Helena", @@ -1795,7 +1795,7 @@ export const countries = { continent: "AF", capital: "Jamestown", currency: ["SHP"], - languages: ["en"] + languages: ["en"], }, SI: { name: "Slovenia", @@ -1804,7 +1804,7 @@ export const countries = { continent: "EU", capital: "Ljubljana", currency: ["EUR"], - languages: ["sl"] + languages: ["sl"], }, SJ: { name: "Svalbard and Jan Mayen", @@ -1813,7 +1813,7 @@ export const countries = { continent: "EU", capital: "Longyearbyen", currency: ["NOK"], - languages: ["no"] + languages: ["no"], }, SK: { name: "Slovakia", @@ -1822,7 +1822,7 @@ export const countries = { continent: "EU", capital: "Bratislava", currency: ["EUR"], - languages: ["sk"] + languages: ["sk"], }, SL: { name: "Sierra Leone", @@ -1831,7 +1831,7 @@ export const countries = { continent: "AF", capital: "Freetown", currency: ["SLL"], - languages: ["en"] + languages: ["en"], }, SM: { name: "San Marino", @@ -1840,7 +1840,7 @@ export const countries = { continent: "EU", capital: "City of San Marino", currency: ["EUR"], - languages: ["it"] + languages: ["it"], }, SN: { name: "Senegal", @@ -1849,7 +1849,7 @@ export const countries = { continent: "AF", capital: "Dakar", currency: ["XOF"], - languages: ["fr"] + languages: ["fr"], }, SO: { name: "Somalia", @@ -1858,7 +1858,7 @@ export const countries = { continent: "AF", capital: "Mogadishu", currency: ["SOS"], - languages: ["so", "ar"] + languages: ["so", "ar"], }, SR: { name: "Suriname", @@ -1867,7 +1867,7 @@ export const countries = { continent: "SA", capital: "Paramaribo", currency: ["SRD"], - languages: ["nl"] + languages: ["nl"], }, SS: { name: "South Sudan", @@ -1876,7 +1876,7 @@ export const countries = { continent: "AF", capital: "Juba", currency: ["SSP"], - languages: ["en"] + languages: ["en"], }, ST: { name: "Sao Tome and Principe", @@ -1885,7 +1885,7 @@ export const countries = { continent: "AF", capital: "São Tomé", currency: ["STN"], - languages: ["pt"] + languages: ["pt"], }, SV: { name: "El Salvador", @@ -1894,7 +1894,7 @@ export const countries = { continent: "NA", capital: "San Salvador", currency: ["SVC", "USD"], - languages: ["es"] + languages: ["es"], }, SX: { name: "Sint Maarten", @@ -1903,7 +1903,7 @@ export const countries = { continent: "NA", capital: "Philipsburg", currency: ["ANG"], - languages: ["nl", "en"] + languages: ["nl", "en"], }, SY: { name: "Syria", @@ -1912,7 +1912,7 @@ export const countries = { continent: "AS", capital: "Damascus", currency: ["SYP"], - languages: ["ar"] + languages: ["ar"], }, SZ: { name: "Eswatini", @@ -1921,7 +1921,7 @@ export const countries = { continent: "AF", capital: "Lobamba", currency: ["SZL"], - languages: ["en", "ss"] + languages: ["en", "ss"], }, TC: { name: "Turks and Caicos Islands", @@ -1930,7 +1930,7 @@ export const countries = { continent: "NA", capital: "Cockburn Town", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, TD: { name: "Chad", @@ -1939,7 +1939,7 @@ export const countries = { continent: "AF", capital: "N'Djamena", currency: ["XAF"], - languages: ["fr", "ar"] + languages: ["fr", "ar"], }, TF: { name: "French Southern Territories", @@ -1948,7 +1948,7 @@ export const countries = { continent: "AN", capital: "Port-aux-Français", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, TG: { name: "Togo", @@ -1957,7 +1957,7 @@ export const countries = { continent: "AF", capital: "Lomé", currency: ["XOF"], - languages: ["fr"] + languages: ["fr"], }, TH: { name: "Thailand", @@ -1966,7 +1966,7 @@ export const countries = { continent: "AS", capital: "Bangkok", currency: ["THB"], - languages: ["th"] + languages: ["th"], }, TJ: { name: "Tajikistan", @@ -1975,7 +1975,7 @@ export const countries = { continent: "AS", capital: "Dushanbe", currency: ["TJS"], - languages: ["tg", "ru"] + languages: ["tg", "ru"], }, TK: { name: "Tokelau", @@ -1984,7 +1984,7 @@ export const countries = { continent: "OC", capital: "Fakaofo", currency: ["NZD"], - languages: ["en"] + languages: ["en"], }, TL: { name: "East Timor", @@ -1993,7 +1993,7 @@ export const countries = { continent: "OC", capital: "Dili", currency: ["USD"], - languages: ["pt"] + languages: ["pt"], }, TM: { name: "Turkmenistan", @@ -2002,7 +2002,7 @@ export const countries = { continent: "AS", capital: "Ashgabat", currency: ["TMT"], - languages: ["tk", "ru"] + languages: ["tk", "ru"], }, TN: { name: "Tunisia", @@ -2011,7 +2011,7 @@ export const countries = { continent: "AF", capital: "Tunis", currency: ["TND"], - languages: ["ar"] + languages: ["ar"], }, TO: { name: "Tonga", @@ -2020,7 +2020,7 @@ export const countries = { continent: "OC", capital: "Nuku'alofa", currency: ["TOP"], - languages: ["en", "to"] + languages: ["en", "to"], }, TR: { name: "Turkey", @@ -2030,7 +2030,7 @@ export const countries = { continents: ["AS", "EU"], capital: "Ankara", currency: ["TRY"], - languages: ["tr"] + languages: ["tr"], }, TT: { name: "Trinidad and Tobago", @@ -2039,7 +2039,7 @@ export const countries = { continent: "NA", capital: "Port of Spain", currency: ["TTD"], - languages: ["en"] + languages: ["en"], }, TV: { name: "Tuvalu", @@ -2048,7 +2048,7 @@ export const countries = { continent: "OC", capital: "Funafuti", currency: ["AUD"], - languages: ["en"] + languages: ["en"], }, TW: { name: "Taiwan", @@ -2057,7 +2057,7 @@ export const countries = { continent: "AS", capital: "Taipei", currency: ["TWD"], - languages: ["zh"] + languages: ["zh"], }, TZ: { name: "Tanzania", @@ -2066,7 +2066,7 @@ export const countries = { continent: "AF", capital: "Dodoma", currency: ["TZS"], - languages: ["sw", "en"] + languages: ["sw", "en"], }, UA: { name: "Ukraine", @@ -2075,7 +2075,7 @@ export const countries = { continent: "EU", capital: "Kyiv", currency: ["UAH"], - languages: ["uk"] + languages: ["uk"], }, UG: { name: "Uganda", @@ -2084,7 +2084,7 @@ export const countries = { continent: "AF", capital: "Kampala", currency: ["UGX"], - languages: ["en", "sw"] + languages: ["en", "sw"], }, UM: { name: "U.S. Minor Outlying Islands", @@ -2093,7 +2093,7 @@ export const countries = { continent: "OC", capital: "", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, US: { name: "United States", @@ -2102,7 +2102,7 @@ export const countries = { continent: "NA", capital: "Washington D.C.", currency: ["USD", "USN", "USS"], - languages: ["en"] + languages: ["en"], }, UY: { name: "Uruguay", @@ -2111,7 +2111,7 @@ export const countries = { continent: "SA", capital: "Montevideo", currency: ["UYI", "UYU"], - languages: ["es"] + languages: ["es"], }, UZ: { name: "Uzbekistan", @@ -2120,7 +2120,7 @@ export const countries = { continent: "AS", capital: "Tashkent", currency: ["UZS"], - languages: ["uz", "ru"] + languages: ["uz", "ru"], }, VA: { name: "Vatican City", @@ -2129,7 +2129,7 @@ export const countries = { continent: "EU", capital: "Vatican City", currency: ["EUR"], - languages: ["it", "la"] + languages: ["it", "la"], }, VC: { name: "Saint Vincent and the Grenadines", @@ -2138,7 +2138,7 @@ export const countries = { continent: "NA", capital: "Kingstown", currency: ["XCD"], - languages: ["en"] + languages: ["en"], }, VE: { name: "Venezuela", @@ -2147,7 +2147,7 @@ export const countries = { continent: "SA", capital: "Caracas", currency: ["VES"], - languages: ["es"] + languages: ["es"], }, VG: { name: "British Virgin Islands", @@ -2156,7 +2156,7 @@ export const countries = { continent: "NA", capital: "Road Town", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, VI: { name: "U.S. Virgin Islands", @@ -2165,7 +2165,7 @@ export const countries = { continent: "NA", capital: "Charlotte Amalie", currency: ["USD"], - languages: ["en"] + languages: ["en"], }, VN: { name: "Vietnam", @@ -2174,7 +2174,7 @@ export const countries = { continent: "AS", capital: "Hanoi", currency: ["VND"], - languages: ["vi"] + languages: ["vi"], }, VU: { name: "Vanuatu", @@ -2183,7 +2183,7 @@ export const countries = { continent: "OC", capital: "Port Vila", currency: ["VUV"], - languages: ["bi", "en", "fr"] + languages: ["bi", "en", "fr"], }, WF: { name: "Wallis and Futuna", @@ -2192,7 +2192,7 @@ export const countries = { continent: "OC", capital: "Mata-Utu", currency: ["XPF"], - languages: ["fr"] + languages: ["fr"], }, WS: { name: "Samoa", @@ -2201,7 +2201,7 @@ export const countries = { continent: "OC", capital: "Apia", currency: ["WST"], - languages: ["sm", "en"] + languages: ["sm", "en"], }, XK: { name: "Kosovo", @@ -2211,7 +2211,7 @@ export const countries = { capital: "Pristina", currency: ["EUR"], languages: ["sq", "sr"], - userAssigned: true + userAssigned: true, }, YE: { name: "Yemen", @@ -2220,7 +2220,7 @@ export const countries = { continent: "AS", capital: "Sana'a", currency: ["YER"], - languages: ["ar"] + languages: ["ar"], }, YT: { name: "Mayotte", @@ -2229,7 +2229,7 @@ export const countries = { continent: "AF", capital: "Mamoudzou", currency: ["EUR"], - languages: ["fr"] + languages: ["fr"], }, ZA: { name: "South Africa", @@ -2238,7 +2238,7 @@ export const countries = { continent: "AF", capital: "Pretoria", currency: ["ZAR"], - languages: ["af", "en", "nr", "st", "ss", "tn", "ts", "ve", "xh", "zu"] + languages: ["af", "en", "nr", "st", "ss", "tn", "ts", "ve", "xh", "zu"], }, ZM: { name: "Zambia", @@ -2247,7 +2247,7 @@ export const countries = { continent: "AF", capital: "Lusaka", currency: ["ZMW"], - languages: ["en"] + languages: ["en"], }, ZW: { name: "Zimbabwe", @@ -2256,6 +2256,6 @@ export const countries = { continent: "AF", capital: "Harare", currency: ["USD", "ZAR", "BWP", "GBP", "AUD", "CNY", "INR", "JPY"], - languages: ["en", "sn", "nd"] - } + languages: ["en", "sn", "nd"], + }, }; diff --git a/apps/api/src/lib/validateUrl.test.ts b/apps/api/src/lib/validateUrl.test.ts index 81c150fb..e417b444 100644 --- a/apps/api/src/lib/validateUrl.test.ts +++ b/apps/api/src/lib/validateUrl.test.ts @@ -20,7 +20,7 @@ describe("isSameDomain", () => { it("should return true for a subdomain with different protocols", () => { const result = isSameDomain( "https://sub.example.com", - "http://example.com" + "http://example.com", ); expect(result).toBe(true); }); @@ -35,7 +35,7 @@ describe("isSameDomain", () => { it("should return true for a subdomain with www prefix", () => { const result = isSameDomain( "http://www.sub.example.com", - "http://example.com" + "http://example.com", ); expect(result).toBe(true); }); @@ -43,7 +43,7 @@ describe("isSameDomain", () => { it("should return true for the same domain with www prefix", () => { const result = isSameDomain( "http://docs.s.s.example.com", - "http://example.com" + "http://example.com", ); expect(result).toBe(true); }); @@ -53,7 +53,7 @@ describe("isSameSubdomain", () => { it("should return false for a subdomain", () => { const result = isSameSubdomain( "http://example.com", - "http://docs.example.com" + "http://docs.example.com", ); expect(result).toBe(false); }); @@ -61,7 +61,7 @@ describe("isSameSubdomain", () => { it("should return true for the same subdomain", () => { const result = isSameSubdomain( "http://docs.example.com", - "http://docs.example.com" + "http://docs.example.com", ); expect(result).toBe(true); }); @@ -69,7 +69,7 @@ describe("isSameSubdomain", () => { it("should return false for different subdomains", () => { const result = isSameSubdomain( "http://docs.example.com", - "http://blog.example.com" + "http://blog.example.com", ); expect(result).toBe(false); }); @@ -89,7 +89,7 @@ describe("isSameSubdomain", () => { it("should return true for the same subdomain with different protocols", () => { const result = isSameSubdomain( "https://docs.example.com", - "http://docs.example.com" + "http://docs.example.com", ); expect(result).toBe(true); }); @@ -97,7 +97,7 @@ describe("isSameSubdomain", () => { it("should return true for the same subdomain with www prefix", () => { const result = isSameSubdomain( "http://www.docs.example.com", - "http://docs.example.com" + "http://docs.example.com", ); expect(result).toBe(true); }); @@ -105,7 +105,7 @@ describe("isSameSubdomain", () => { it("should return false for a subdomain with www prefix and different subdomain", () => { const result = isSameSubdomain( "http://www.docs.example.com", - "http://blog.example.com" + "http://blog.example.com", ); expect(result).toBe(false); }); @@ -117,7 +117,7 @@ describe("removeDuplicateUrls", () => { "http://example.com", "https://example.com", "http://www.example.com", - "https://www.example.com" + "https://www.example.com", ]; const result = removeDuplicateUrls(urls); expect(result).toEqual(["https://example.com"]); @@ -128,14 +128,14 @@ describe("removeDuplicateUrls", () => { "https://example.com/page1", "https://example.com/page2", "https://example.com/page1?param=1", - "https://example.com/page1#section1" + "https://example.com/page1#section1", ]; const result = removeDuplicateUrls(urls); expect(result).toEqual([ "https://example.com/page1", "https://example.com/page2", "https://example.com/page1?param=1", - "https://example.com/page1#section1" + "https://example.com/page1#section1", ]); }); diff --git a/apps/api/src/lib/withAuth.ts b/apps/api/src/lib/withAuth.ts index ab3f4d4b..a585fe0a 100644 --- a/apps/api/src/lib/withAuth.ts +++ b/apps/api/src/lib/withAuth.ts @@ -8,7 +8,7 @@ let warningCount = 0; export function withAuth( originalFunction: (...args: U) => Promise, - mockSuccess: T + mockSuccess: T, ) { return async function (...args: U): Promise { const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; diff --git a/apps/api/src/main/runWebScraper.ts b/apps/api/src/main/runWebScraper.ts index 981189ab..dc907371 100644 --- a/apps/api/src/main/runWebScraper.ts +++ b/apps/api/src/main/runWebScraper.ts @@ -2,7 +2,7 @@ import { Job } from "bullmq"; import { WebScraperOptions, RunWebScraperParams, - RunWebScraperResult + RunWebScraperResult, } from "../types"; import { billTeam } from "../services/billing/credit_billing"; import { Document } from "../controllers/v1/types"; @@ -13,14 +13,14 @@ import { configDotenv } from "dotenv"; import { EngineResultsTracker, scrapeURL, - ScrapeUrlResponse + ScrapeUrlResponse, } from "../scraper/scrapeURL"; import { Engine } from "../scraper/scrapeURL/engines"; configDotenv(); export async function startWebScraperPipeline({ job, - token + token, }: { job: Job & { id: string }; token: string; @@ -32,9 +32,9 @@ export async function startWebScraperPipeline({ ...job.data.scrapeOptions, ...(job.data.crawl_id ? { - formats: job.data.scrapeOptions.formats.concat(["rawHtml"]) + formats: job.data.scrapeOptions.formats.concat(["rawHtml"]), } - : {}) + : {}), }, internalOptions: job.data.internalOptions, // onSuccess: (result, mode) => { @@ -48,7 +48,7 @@ export async function startWebScraperPipeline({ team_id: job.data.team_id, bull_job_id: job.id.toString(), priority: job.opts.priority, - is_scrape: job.data.is_scrape ?? false + is_scrape: job.data.is_scrape ?? false, }); } @@ -62,14 +62,14 @@ export async function runWebScraper({ team_id, bull_job_id, priority, - is_scrape = false + is_scrape = false, }: RunWebScraperParams): Promise { let response: ScrapeUrlResponse | undefined = undefined; let engines: EngineResultsTracker = {}; try { response = await scrapeURL(bull_job_id, url, scrapeOptions, { priority, - ...internalOptions + ...internalOptions, }); if (!response.success) { if (response.error instanceof Error) { @@ -81,7 +81,7 @@ export async function runWebScraper({ ? JSON.stringify(response.error) : typeof response.error === "object" ? JSON.stringify({ ...response.error }) - : response.error) + : response.error), ); } } @@ -94,7 +94,7 @@ export async function runWebScraper({ billTeam(team_id, undefined, creditsToBeBilled).catch((error) => { logger.error( - `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}` + `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`, ); // Optionally, you could notify an admin or add to a retry queue here }); @@ -117,14 +117,14 @@ export async function runWebScraper({ return { ...response, success: false, - error + error, }; } else { return { success: false, error, logs: ["no logs -- error coming from runWebScraper"], - engines + engines, }; } // onError(error); @@ -154,8 +154,8 @@ export async function runWebScraper({ : result.state === "timeout" ? "Timed out" : undefined, - time_taken: result.finishedAt - result.startedAt - } + time_taken: result.finishedAt - result.startedAt, + }, }); } } @@ -166,7 +166,7 @@ const saveJob = async ( result: any, token: string, mode: string, - engines?: EngineResultsTracker + engines?: EngineResultsTracker, ) => { try { const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 861ae9fc..ec9967b8 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -4,7 +4,7 @@ import { autoscalerController, checkQueuesController, cleanBefore24hCompleteJobsController, - queuesController + queuesController, } from "../controllers/v0/admin/queue"; import { wrap } from "./v1"; import { acucCacheClearController } from "../controllers/v0/admin/acuc-cache-clear"; @@ -13,27 +13,27 @@ export const adminRouter = express.Router(); adminRouter.get( `/admin/${process.env.BULL_AUTH_KEY}/redis-health`, - redisHealthController + redisHealthController, ); adminRouter.get( `/admin/${process.env.BULL_AUTH_KEY}/clean-before-24h-complete-jobs`, - cleanBefore24hCompleteJobsController + cleanBefore24hCompleteJobsController, ); adminRouter.get( `/admin/${process.env.BULL_AUTH_KEY}/check-queues`, - checkQueuesController + checkQueuesController, ); adminRouter.get(`/admin/${process.env.BULL_AUTH_KEY}/queues`, queuesController); adminRouter.get( `/admin/${process.env.BULL_AUTH_KEY}/autoscaler`, - autoscalerController + autoscalerController, ); adminRouter.post( `/admin/${process.env.BULL_AUTH_KEY}/acuc-cache-clear`, - wrap(acucCacheClearController) + wrap(acucCacheClearController), ); diff --git a/apps/api/src/routes/v1.ts b/apps/api/src/routes/v1.ts index a9727e00..5daa077b 100644 --- a/apps/api/src/routes/v1.ts +++ b/apps/api/src/routes/v1.ts @@ -8,7 +8,7 @@ import { ErrorResponse, RequestWithACUC, RequestWithAuth, - RequestWithMaybeAuth + RequestWithMaybeAuth, } from "../controllers/v1/types"; import { RateLimiterMode } from "../types"; import { authenticateUser } from "../controllers/auth"; @@ -33,7 +33,7 @@ import { extractController } from "../controllers/v1/extract"; // import { readinessController } from "../controllers/v1/readiness"; function checkCreditsMiddleware( - minimum?: number + minimum?: number, ): (req: RequestWithAuth, res: Response, next: NextFunction) => void { return (req, res, next) => { (async () => { @@ -44,20 +44,20 @@ function checkCreditsMiddleware( const { success, remainingCredits, chunk } = await checkTeamCredits( req.acuc, req.auth.team_id, - minimum ?? 1 + minimum ?? 1, ); if (chunk) { req.acuc = chunk; } if (!success) { logger.error( - `Insufficient credits: ${JSON.stringify({ team_id: req.auth.team_id, minimum, remainingCredits })}` + `Insufficient credits: ${JSON.stringify({ team_id: req.auth.team_id, minimum, remainingCredits })}`, ); if (!res.headersSent) { return res.status(402).json({ success: false, error: - "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing or try changing the request limit to a lower value." + "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing or try changing the request limit to a lower value.", }); } } @@ -68,7 +68,7 @@ function checkCreditsMiddleware( } export function authMiddleware( - rateLimiterMode: RateLimiterMode + rateLimiterMode: RateLimiterMode, ): (req: RequestWithMaybeAuth, res: Response, next: NextFunction) => void { return (req, res, next) => { (async () => { @@ -99,7 +99,7 @@ export function authMiddleware( function idempotencyMiddleware( req: Request, res: Response, - next: NextFunction + next: NextFunction, ) { (async () => { if (req.headers["x-idempotency-key"]) { @@ -123,7 +123,7 @@ function blocklistMiddleware(req: Request, res: Response, next: NextFunction) { return res.status(403).json({ success: false, error: - "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions." + "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions.", }); } } @@ -131,7 +131,7 @@ function blocklistMiddleware(req: Request, res: Response, next: NextFunction) { } export function wrap( - controller: (req: Request, res: Response) => Promise + controller: (req: Request, res: Response) => Promise, ): (req: Request, res: Response, next: NextFunction) => any { return (req, res, next) => { controller(req, res).catch((err) => next(err)); @@ -147,7 +147,7 @@ v1Router.post( authMiddleware(RateLimiterMode.Scrape), checkCreditsMiddleware(1), blocklistMiddleware, - wrap(scrapeController) + wrap(scrapeController), ); v1Router.post( @@ -156,7 +156,7 @@ v1Router.post( checkCreditsMiddleware(), blocklistMiddleware, idempotencyMiddleware, - wrap(crawlController) + wrap(crawlController), ); v1Router.post( @@ -165,7 +165,7 @@ v1Router.post( checkCreditsMiddleware(), blocklistMiddleware, idempotencyMiddleware, - wrap(batchScrapeController) + wrap(batchScrapeController), ); v1Router.post( @@ -173,20 +173,20 @@ v1Router.post( authMiddleware(RateLimiterMode.Map), checkCreditsMiddleware(1), blocklistMiddleware, - wrap(mapController) + wrap(mapController), ); v1Router.get( "/crawl/:jobId", authMiddleware(RateLimiterMode.CrawlStatus), - wrap(crawlStatusController) + wrap(crawlStatusController), ); v1Router.get( "/batch/scrape/:jobId", authMiddleware(RateLimiterMode.CrawlStatus), // Yes, it uses the same controller as the normal crawl status controller - wrap((req: any, res): any => crawlStatusController(req, res, true)) + wrap((req: any, res): any => crawlStatusController(req, res, true)), ); v1Router.get("/scrape/:jobId", wrap(scrapeStatusController)); @@ -194,7 +194,7 @@ v1Router.get("/scrape/:jobId", wrap(scrapeStatusController)); v1Router.get( "/concurrency-check", authMiddleware(RateLimiterMode.CrawlStatus), - wrap(concurrencyCheckController) + wrap(concurrencyCheckController), ); v1Router.ws("/crawl/:jobId", crawlStatusWSController); @@ -203,7 +203,7 @@ v1Router.post( "/extract", authMiddleware(RateLimiterMode.Scrape), checkCreditsMiddleware(1), - wrap(extractController) + wrap(extractController), ); // v1Router.post("/crawlWebsitePreview", crawlPreviewController); @@ -211,7 +211,7 @@ v1Router.post( v1Router.delete( "/crawl/:jobId", authMiddleware(RateLimiterMode.CrawlStatus), - crawlCancelController + crawlCancelController, ); // v1Router.get("/checkJobStatus/:jobId", crawlJobStatusPreviewController); diff --git a/apps/api/src/run-req.ts b/apps/api/src/run-req.ts index 61ee61bd..a7f4694a 100644 --- a/apps/api/src/run-req.ts +++ b/apps/api/src/run-req.ts @@ -18,20 +18,20 @@ async function sendCrawl(result: Result): Promise { { url: url, crawlerOptions: { - limit: 75 + limit: 75, }, pageOptions: { includeHtml: true, replaceAllPathsWithAbsolutePaths: true, - waitFor: 1000 - } + waitFor: 1000, + }, }, { headers: { "Content-Type": "application/json", - Authorization: `Bearer ` - } - } + Authorization: `Bearer `, + }, + }, ); result.idempotency_key = idempotencyKey; return response.data.jobId; @@ -51,9 +51,9 @@ async function getContent(result: Result): Promise { { headers: { "Content-Type": "application/json", - Authorization: `Bearer ` - } - } + Authorization: `Bearer `, + }, + }, ); if (response.data.status === "completed") { result.result_data_jsonb = response.data.data; @@ -97,11 +97,11 @@ async function processResults(results: Result[]): Promise { // Save job id along with the start_url const resultWithJobId = results.map((r) => ({ start_url: r.start_url, - job_id: r.job_id + job_id: r.job_id, })); await fs.writeFile( "results_with_job_id_4000_6000.json", - JSON.stringify(resultWithJobId, null, 4) + JSON.stringify(resultWithJobId, null, 4), ); } catch (error) { console.error("Error writing to results_with_content.json:", error); diff --git a/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts b/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts index da2b7d61..897ea46c 100644 --- a/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts +++ b/apps/api/src/scraper/WebScraper/__tests__/crawler.test.ts @@ -32,7 +32,7 @@ describe("WebCrawler", () => { getMatchingLineNumber: jest.fn().mockReturnValue(0), getCrawlDelay: jest.fn().mockReturnValue(0), getSitemaps: jest.fn().mockReturnValue([]), - getPreferredHost: jest.fn().mockReturnValue("example.com") + getPreferredHost: jest.fn().mockReturnValue("example.com"), }); }); @@ -46,7 +46,7 @@ describe("WebCrawler", () => { includes: [], excludes: [], limit: limit, // Apply the limit - maxCrawledDepth: 10 + maxCrawledDepth: 10, }); // Mock sitemap fetching function to return more links than the limit @@ -56,7 +56,7 @@ describe("WebCrawler", () => { initialUrl, initialUrl + "/page1", initialUrl + "/page2", - initialUrl + "/page3" + initialUrl + "/page3", ]); const filteredLinks = crawler["filterLinks"]( @@ -64,10 +64,10 @@ describe("WebCrawler", () => { initialUrl, initialUrl + "/page1", initialUrl + "/page2", - initialUrl + "/page3" + initialUrl + "/page3", ], limit, - 10 + 10, ); expect(filteredLinks.length).toBe(limit); // Check if the number of results respects the limit diff --git a/apps/api/src/scraper/WebScraper/crawler.ts b/apps/api/src/scraper/WebScraper/crawler.ts index be3cdf72..19b0b5b4 100644 --- a/apps/api/src/scraper/WebScraper/crawler.ts +++ b/apps/api/src/scraper/WebScraper/crawler.ts @@ -40,7 +40,7 @@ export class WebCrawler { allowBackwardCrawling = false, allowExternalContentLinks = false, allowSubdomains = false, - ignoreRobotsTxt = false + ignoreRobotsTxt = false, }: { jobId: string; initialUrl: string; @@ -79,7 +79,7 @@ export class WebCrawler { sitemapLinks: string[], limit: number, maxDepth: number, - fromMap: boolean = false + fromMap: boolean = false, ): string[] { // If the initial URL is a sitemap.xml, skip filtering if (this.initialUrl.endsWith("sitemap.xml") && fromMap) { @@ -95,7 +95,7 @@ export class WebCrawler { this.logger.debug(`Error processing link: ${link}`, { link, error, - method: "filterLinks" + method: "filterLinks", }); return false; } @@ -112,7 +112,7 @@ export class WebCrawler { if (this.excludes.length > 0 && this.excludes[0] !== "") { if ( this.excludes.some((excludePattern) => - new RegExp(excludePattern).test(path) + new RegExp(excludePattern).test(path), ) ) { return false; @@ -123,7 +123,7 @@ export class WebCrawler { if (this.includes.length > 0 && this.includes[0] !== "") { if ( !this.includes.some((includePattern) => - new RegExp(includePattern).test(path) + new RegExp(includePattern).test(path), ) ) { return false; @@ -140,7 +140,7 @@ export class WebCrawler { } const initialHostname = normalizedInitialUrl.hostname.replace( /^www\./, - "" + "", ); const linkHostname = normalizedLink.hostname.replace(/^www\./, ""); @@ -165,7 +165,7 @@ export class WebCrawler { if (!isAllowed) { this.logger.debug(`Link disallowed by robots.txt: ${link}`, { method: "filterLinks", - link + link, }); return false; } @@ -183,12 +183,12 @@ export class WebCrawler { let extraArgs = {}; if (skipTlsVerification) { extraArgs["httpsAgent"] = new https.Agent({ - rejectUnauthorized: false + rejectUnauthorized: false, }); } const response = await axios.get(this.robotsTxtUrl, { timeout: axiosTimeout, - ...extraArgs + ...extraArgs, }); return response.data; } @@ -199,10 +199,10 @@ export class WebCrawler { public async tryGetSitemap( fromMap: boolean = false, - onlySitemap: boolean = false + onlySitemap: boolean = false, ): Promise<{ url: string; html: string }[] | null> { this.logger.debug(`Fetching sitemap links from ${this.initialUrl}`, { - method: "tryGetSitemap" + method: "tryGetSitemap", }); const sitemapLinks = await this.tryFetchSitemapLinks(this.initialUrl); if (fromMap && onlySitemap) { @@ -213,7 +213,7 @@ export class WebCrawler { sitemapLinks, this.limit, this.maxCrawledDepth, - fromMap + fromMap, ); return filteredLinks.map((link) => ({ url: link, html: "" })); } @@ -303,7 +303,7 @@ export class WebCrawler { private isRobotsAllowed( url: string, - ignoreRobotsTxt: boolean = false + ignoreRobotsTxt: boolean = false, ): boolean { return ignoreRobotsTxt ? true @@ -352,7 +352,7 @@ export class WebCrawler { url .split("/") .slice(3) - .filter((subArray) => subArray.length > 0).length + .filter((subArray) => subArray.length > 0).length, ); } @@ -373,7 +373,7 @@ export class WebCrawler { private isSubdomain(link: string): boolean { return new URL(link, this.baseUrl).hostname.endsWith( - "." + new URL(this.baseUrl).hostname.split(".").slice(-2).join(".") + "." + new URL(this.baseUrl).hostname.split(".").slice(-2).join("."), ); } @@ -405,7 +405,7 @@ export class WebCrawler { ".ttf", ".woff2", ".webp", - ".inc" + ".inc", ]; try { @@ -414,7 +414,7 @@ export class WebCrawler { } catch (error) { this.logger.error(`Error processing URL in isFile`, { method: "isFile", - error + error, }); return false; } @@ -431,7 +431,7 @@ export class WebCrawler { "github.com", "calendly.com", "discord.gg", - "discord.com" + "discord.com", ]; return socialMediaOrEmail.some((ext) => url.includes(ext)); } @@ -457,14 +457,14 @@ export class WebCrawler { } catch (error) { this.logger.debug( `Failed to fetch sitemap with axios from ${sitemapUrl}`, - { method: "tryFetchSitemapLinks", sitemapUrl, error } + { method: "tryFetchSitemapLinks", sitemapUrl, error }, ); if (error instanceof AxiosError && error.response?.status === 404) { // ignore 404 } else { const response = await getLinksFromSitemap( { sitemapUrl, mode: "fire-engine" }, - this.logger + this.logger, ); if (response) { sitemapLinks = response; @@ -476,26 +476,26 @@ export class WebCrawler { const baseUrlSitemap = `${this.baseUrl}/sitemap.xml`; try { const response = await axios.get(baseUrlSitemap, { - timeout: axiosTimeout + timeout: axiosTimeout, }); if (response.status === 200) { sitemapLinks = await getLinksFromSitemap( { sitemapUrl: baseUrlSitemap, mode: "fire-engine" }, - this.logger + this.logger, ); } } catch (error) { this.logger.debug(`Failed to fetch sitemap from ${baseUrlSitemap}`, { method: "tryFetchSitemapLinks", sitemapUrl: baseUrlSitemap, - error + error, }); if (error instanceof AxiosError && error.response?.status === 404) { // ignore 404 } else { sitemapLinks = await getLinksFromSitemap( { sitemapUrl: baseUrlSitemap, mode: "fire-engine" }, - this.logger + this.logger, ); } } @@ -503,7 +503,7 @@ export class WebCrawler { const normalizedUrl = normalizeUrl(url); const normalizedSitemapLinks = sitemapLinks.map((link) => - normalizeUrl(link) + normalizeUrl(link), ); // has to be greater than 0 to avoid adding the initial URL to the sitemap links, and preventing crawler to crawl if ( diff --git a/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts b/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts index ba77b78b..01c40de9 100644 --- a/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts +++ b/apps/api/src/scraper/WebScraper/custom/handleCustomScraping.ts @@ -2,7 +2,7 @@ import { logger } from "../../../lib/logger"; export async function handleCustomScraping( text: string, - url: string + url: string, ): Promise<{ scraper: string; url: string; @@ -15,7 +15,7 @@ export async function handleCustomScraping( !url.includes("developers.notion.com") ) { logger.debug( - `Special use case detected for ${url}, using Fire Engine with wait time 1000ms` + `Special use case detected for ${url}, using Fire Engine with wait time 1000ms`, ); return { scraper: "fire-engine", @@ -23,21 +23,21 @@ export async function handleCustomScraping( waitAfterLoad: 1000, pageOptions: { scrollXPaths: [ - '//*[@id="ReferencePlayground"]/section[3]/div/pre/div/div/div[5]' - ] - } + '//*[@id="ReferencePlayground"]/section[3]/div/pre/div/div/div[5]', + ], + }, }; } // Check for Vanta security portals if (text.includes(' { try { let content: string = ""; @@ -29,7 +29,7 @@ export async function getLinksFromSitemap( "sitemap", sitemapUrl, scrapeOptions.parse({ formats: ["rawHtml"] }), - { forceEngine: "fire-engine;tlsclient", v0DisableJsDom: true } + { forceEngine: "fire-engine;tlsclient", v0DisableJsDom: true }, ); if (!response.success) { throw response.error; @@ -41,7 +41,7 @@ export async function getLinksFromSitemap( method: "getLinksFromSitemap", mode, sitemapUrl, - error + error, }); return allUrls; @@ -56,8 +56,8 @@ export async function getLinksFromSitemap( .map((sitemap) => getLinksFromSitemap( { sitemapUrl: sitemap.loc[0], allUrls, mode }, - logger - ) + logger, + ), ); await Promise.all(sitemapPromises); } else if (root && root.url) { @@ -66,7 +66,7 @@ export async function getLinksFromSitemap( (url) => url.loc && url.loc.length > 0 && - !WebCrawler.prototype.isFile(url.loc[0]) + !WebCrawler.prototype.isFile(url.loc[0]), ) .map((url) => url.loc[0]); allUrls.push(...validUrls); @@ -76,7 +76,7 @@ export async function getLinksFromSitemap( method: "getLinksFromSitemap", mode, sitemapUrl, - error + error, }); } @@ -85,12 +85,12 @@ export async function getLinksFromSitemap( export const fetchSitemapData = async ( url: string, - timeout?: number + timeout?: number, ): Promise => { const sitemapUrl = url.endsWith("/sitemap.xml") ? url : `${url}/sitemap.xml`; try { const response = await axios.get(sitemapUrl, { - timeout: timeout || axiosTimeout + timeout: timeout || axiosTimeout, }); if (response.status === 200) { const xml = response.data; diff --git a/apps/api/src/scraper/WebScraper/utils/__tests__/blocklist.test.ts b/apps/api/src/scraper/WebScraper/utils/__tests__/blocklist.test.ts index d256aa44..d3963685 100644 --- a/apps/api/src/scraper/WebScraper/utils/__tests__/blocklist.test.ts +++ b/apps/api/src/scraper/WebScraper/utils/__tests__/blocklist.test.ts @@ -15,7 +15,7 @@ describe("Blocklist Functionality", () => { "https://flickr.com/photos/johndoe", "https://whatsapp.com/download", "https://wechat.com/features", - "https://telegram.org/apps" + "https://telegram.org/apps", ])("should return true for blocklisted URL %s", (url) => { expect(isUrlBlocked(url)).toBe(true); }); @@ -33,7 +33,7 @@ describe("Blocklist Functionality", () => { "https://flickr.com/help/terms", "https://whatsapp.com/legal", "https://wechat.com/en/privacy-policy", - "https://telegram.org/tos" + "https://telegram.org/tos", ])("should return false for allowed URLs with keywords %s", (url) => { expect(isUrlBlocked(url)).toBe(false); }); @@ -54,35 +54,35 @@ describe("Blocklist Functionality", () => { "https://facebook.com.someotherdomain.com", "https://www.facebook.com/profile", "https://api.twitter.com/info", - "https://instagram.com/accounts/login" + "https://instagram.com/accounts/login", ])( "should return true for URLs with blocklisted domains in subdomains or paths %s", (url) => { expect(isUrlBlocked(url)).toBe(true); - } + }, ); test.each([ "https://example.com/facebook.com", "https://example.com/redirect?url=https://twitter.com", - "https://facebook.com.policy.example.com" + "https://facebook.com.policy.example.com", ])( "should return false for URLs where blocklisted domain is part of another domain or path %s", (url) => { expect(isUrlBlocked(url)).toBe(false); - } + }, ); test.each(["https://FACEBOOK.com", "https://INSTAGRAM.com/@something"])( "should handle case variations %s", (url) => { expect(isUrlBlocked(url)).toBe(true); - } + }, ); test.each([ "https://facebook.com?redirect=https://example.com", - "https://twitter.com?query=something" + "https://twitter.com?query=something", ])("should handle query parameters %s", (url) => { expect(isUrlBlocked(url)).toBe(true); }); diff --git a/apps/api/src/scraper/WebScraper/utils/blocklist.ts b/apps/api/src/scraper/WebScraper/utils/blocklist.ts index e60943e6..58fcade4 100644 --- a/apps/api/src/scraper/WebScraper/utils/blocklist.ts +++ b/apps/api/src/scraper/WebScraper/utils/blocklist.ts @@ -18,7 +18,7 @@ const socialMediaBlocklist = [ "youtube.com", "corterix.com", "southwest.com", - "ryanair.com" + "ryanair.com", ]; const allowedKeywords = [ @@ -41,7 +41,7 @@ const allowedKeywords = [ "://library.tiktok.com", "://ads.tiktok.com", "://tiktok.com/business", - "://developers.facebook.com" + "://developers.facebook.com", ]; export function isUrlBlocked(url: string): boolean { @@ -50,7 +50,7 @@ export function isUrlBlocked(url: string): boolean { // Check if the URL contains any allowed keywords as whole words if ( allowedKeywords.some((keyword) => - new RegExp(`\\b${keyword}\\b`, "i").test(lowerCaseUrl) + new RegExp(`\\b${keyword}\\b`, "i").test(lowerCaseUrl), ) ) { return false; @@ -68,7 +68,7 @@ export function isUrlBlocked(url: string): boolean { const isBlocked = socialMediaBlocklist.some((domain) => { const domainPattern = new RegExp( `(^|\\.)${domain.replace(".", "\\.")}(\\.|$)`, - "i" + "i", ); return domainPattern.test(hostname); }); diff --git a/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts b/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts index 3db7c5c1..a58f9c4e 100644 --- a/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts +++ b/apps/api/src/scraper/WebScraper/utils/maxDepthUtils.ts @@ -1,6 +1,6 @@ export function getAdjustedMaxDepth( url: string, - maxCrawlDepth: number + maxCrawlDepth: number, ): number { const baseURLDepth = getURLDepth(url); const adjustedMaxDepth = maxCrawlDepth + baseURLDepth; diff --git a/apps/api/src/scraper/scrapeURL/engines/cache/index.ts b/apps/api/src/scraper/scrapeURL/engines/cache/index.ts index f6ffcb13..f48806fd 100644 --- a/apps/api/src/scraper/scrapeURL/engines/cache/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/cache/index.ts @@ -14,6 +14,6 @@ export async function scrapeCache(meta: Meta): Promise { url: entry.url, html: entry.html, statusCode: entry.statusCode, - error: entry.error + error: entry.error, }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/docx/index.ts b/apps/api/src/scraper/scrapeURL/engines/docx/index.ts index 02ed0c3f..933d4d74 100644 --- a/apps/api/src/scraper/scrapeURL/engines/docx/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/docx/index.ts @@ -10,6 +10,6 @@ export async function scrapeDOCX(meta: Meta): Promise { url: response.url, statusCode: response.status, - html: (await mammoth.convertToHtml({ path: tempFilePath })).value + html: (await mammoth.convertToHtml({ path: tempFilePath })).value, }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts b/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts index 92f2d451..af6f57c0 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts @@ -4,33 +4,33 @@ import { TimeoutError } from "../../error"; import { specialtyScrapeCheck } from "../utils/specialtyHandler"; export async function scrapeURLWithFetch( - meta: Meta + meta: Meta, ): Promise { const timeout = 20000; const response = await Promise.race([ fetch(meta.url, { redirect: "follow", - headers: meta.options.headers + headers: meta.options.headers, }), (async () => { await new Promise((resolve) => setTimeout(() => resolve(null), timeout)); throw new TimeoutError( "Fetch was unable to scrape the page before timing out", - { cause: { timeout } } + { cause: { timeout } }, ); - })() + })(), ]); specialtyScrapeCheck( meta.logger.child({ method: "scrapeURLWithFetch/specialtyScrapeCheck" }), - Object.fromEntries(response.headers as any) + Object.fromEntries(response.headers as any), ); return { url: response.url, html: await response.text(), - statusCode: response.status + statusCode: response.status, // TODO: error? }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts index c3742d26..328931ba 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts @@ -31,10 +31,10 @@ const successSchema = z.object({ actionContent: z .object({ url: z.string(), - html: z.string() + html: z.string(), }) .array() - .optional() + .optional(), }); export type FireEngineCheckStatusSuccess = z.infer; @@ -47,16 +47,16 @@ const processingSchema = z.object({ "waiting", "waiting-children", "unknown", - "prioritized" + "prioritized", ]), - processing: z.boolean() + processing: z.boolean(), }); const failedSchema = z.object({ jobId: z.string(), state: z.literal("failed"), processing: z.literal(false), - error: z.string() + error: z.string(), }); export class StillProcessingError extends Error { @@ -67,7 +67,7 @@ export class StillProcessingError extends Error { export async function fireEngineCheckStatus( logger: Logger, - jobId: string + jobId: string, ): Promise { const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; @@ -75,8 +75,8 @@ export async function fireEngineCheckStatus( { name: "fire-engine: Check status", attributes: { - jobId - } + jobId, + }, }, async (span) => { return await robustFetch({ @@ -87,12 +87,12 @@ export async function fireEngineCheckStatus( ...(Sentry.isInitialized() ? { "sentry-trace": Sentry.spanToTraceHeader(span), - baggage: Sentry.spanToBaggageHeader(span) + baggage: Sentry.spanToBaggageHeader(span), } - : {}) - } + : {}), + }, }); - } + }, ); const successParse = successSchema.safeParse(status); @@ -115,23 +115,23 @@ export async function fireEngineCheckStatus( throw new EngineError("Scrape job failed", { cause: { status, - jobId - } + jobId, + }, }); } } else { logger.debug("Check status returned response not matched by any schema", { status, - jobId + jobId, }); throw new Error( "Check status returned response not matched by any schema", { cause: { status, - jobId - } - } + jobId, + }, + }, ); } } diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts index 96d73390..d5fe58cb 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/delete.ts @@ -10,8 +10,8 @@ export async function fireEngineDelete(logger: Logger, jobId: string) { { name: "fire-engine: Delete scrape", attributes: { - jobId - } + jobId, + }, }, async (span) => { await robustFetch({ @@ -21,15 +21,15 @@ export async function fireEngineDelete(logger: Logger, jobId: string) { ...(Sentry.isInitialized() ? { "sentry-trace": Sentry.spanToTraceHeader(span), - baggage: Sentry.spanToBaggageHeader(span) + baggage: Sentry.spanToBaggageHeader(span), } - : {}) + : {}), }, ignoreResponse: true, ignoreFailure: true, - logger: logger.child({ method: "fireEngineDelete/robustFetch", jobId }) + logger: logger.child({ method: "fireEngineDelete/robustFetch", jobId }), }); - } + }, ); // We do not care whether this fails or not. diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index 851b8faf..3fc32835 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -5,13 +5,13 @@ import { FireEngineScrapeRequestChromeCDP, FireEngineScrapeRequestCommon, FireEngineScrapeRequestPlaywright, - FireEngineScrapeRequestTLSClient + FireEngineScrapeRequestTLSClient, } from "./scrape"; import { EngineScrapeResult } from ".."; import { fireEngineCheckStatus, FireEngineCheckStatusSuccess, - StillProcessingError + StillProcessingError, } from "./checkStatus"; import { EngineError, SiteError, TimeoutError } from "../../error"; import * as Sentry from "@sentry/node"; @@ -27,15 +27,15 @@ async function performFireEngineScrape< Engine extends | FireEngineScrapeRequestChromeCDP | FireEngineScrapeRequestPlaywright - | FireEngineScrapeRequestTLSClient + | FireEngineScrapeRequestTLSClient, >( logger: Logger, request: FireEngineScrapeRequestCommon & Engine, - timeout = defaultTimeout + timeout = defaultTimeout, ): Promise { const scrape = await fireEngineScrape( logger.child({ method: "fireEngineScrape" }), - request + request, ); const startTime = Date.now(); @@ -47,25 +47,25 @@ async function performFireEngineScrape< if (errors.length >= errorLimit) { logger.error("Error limit hit.", { errors }); throw new Error("Error limit hit. See e.cause.errors for errors.", { - cause: { errors } + cause: { errors }, }); } if (Date.now() - startTime > timeout) { logger.info( "Fire-engine was unable to scrape the page before timing out.", - { errors, timeout } + { errors, timeout }, ); throw new TimeoutError( "Fire-engine was unable to scrape the page before timing out", - { cause: { errors, timeout } } + { cause: { errors, timeout } }, ); } try { status = await fireEngineCheckStatus( logger.child({ method: "fireEngineCheckStatus" }), - scrape.jobId + scrape.jobId, ); } catch (error) { if (error instanceof StillProcessingError) { @@ -73,7 +73,7 @@ async function performFireEngineScrape< } else if (error instanceof EngineError || error instanceof SiteError) { logger.debug("Fire-engine scrape job failed.", { error, - jobId: scrape.jobId + jobId: scrape.jobId, }); throw error; } else { @@ -81,7 +81,7 @@ async function performFireEngineScrape< errors.push(error); logger.debug( `An unexpeceted error occurred while calling checkStatus. Error counter is now at ${errors.length}.`, - { error, jobId: scrape.jobId } + { error, jobId: scrape.jobId }, ); } } @@ -93,7 +93,7 @@ async function performFireEngineScrape< } export async function scrapeURLWithFireEngineChromeCDP( - meta: Meta + meta: Meta, ): Promise { const actions: Action[] = [ // Transform waitFor option into an action (unsupported by chrome-cdp) @@ -101,8 +101,8 @@ export async function scrapeURLWithFireEngineChromeCDP( ? [ { type: "wait" as const, - milliseconds: meta.options.waitFor - } + milliseconds: meta.options.waitFor, + }, ] : []), @@ -112,13 +112,13 @@ export async function scrapeURLWithFireEngineChromeCDP( ? [ { type: "screenshot" as const, - fullPage: meta.options.formats.includes("screenshot@fullPage") - } + fullPage: meta.options.formats.includes("screenshot@fullPage"), + }, ] : []), // Include specified actions - ...(meta.options.actions ?? []) + ...(meta.options.actions ?? []), ]; const request: FireEngineScrapeRequestCommon & @@ -130,36 +130,36 @@ export async function scrapeURLWithFireEngineChromeCDP( headers: meta.options.headers, ...(actions.length > 0 ? { - actions + actions, } : {}), priority: meta.internalOptions.priority, geolocation: meta.options.geolocation, mobile: meta.options.mobile, timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic - disableSmartWaitCache: meta.internalOptions.disableSmartWaitCache + disableSmartWaitCache: meta.internalOptions.disableSmartWaitCache, // TODO: scrollXPaths }; const totalWait = actions.reduce( (a, x) => (x.type === "wait" ? (x.milliseconds ?? 1000) + a : a), - 0 + 0, ); let response = await performFireEngineScrape( meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", - request + request, }), request, - meta.options.timeout !== undefined ? defaultTimeout + totalWait : Infinity // TODO: better timeout handling + meta.options.timeout !== undefined ? defaultTimeout + totalWait : Infinity, // TODO: better timeout handling ); specialtyScrapeCheck( meta.logger.child({ - method: "scrapeURLWithFireEngineChromeCDP/specialtyScrapeCheck" + method: "scrapeURLWithFireEngineChromeCDP/specialtyScrapeCheck", }), - response.responseHeaders + response.responseHeaders, ); if ( @@ -168,20 +168,20 @@ export async function scrapeURLWithFireEngineChromeCDP( ) { meta.logger.debug( "Transforming screenshots from actions into screenshot field", - { screenshots: response.screenshots } + { screenshots: response.screenshots }, ); response.screenshot = (response.screenshots ?? [])[0]; (response.screenshots ?? []).splice(0, 1); meta.logger.debug("Screenshot transformation done", { screenshots: response.screenshots, - screenshot: response.screenshot + screenshot: response.screenshot, }); } if (!response.url) { meta.logger.warn("Fire-engine did not return the response's URL", { response, - sourceURL: meta.url + sourceURL: meta.url, }); } @@ -197,15 +197,15 @@ export async function scrapeURLWithFireEngineChromeCDP( ? { actions: { screenshots: response.screenshots ?? [], - scrapes: response.actionContent ?? [] - } + scrapes: response.actionContent ?? [], + }, } - : {}) + : {}), }; } export async function scrapeURLWithFireEnginePlaywright( - meta: Meta + meta: Meta, ): Promise { const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestPlaywright = { @@ -220,31 +220,31 @@ export async function scrapeURLWithFireEnginePlaywright( wait: meta.options.waitFor, geolocation: meta.options.geolocation, - timeout: meta.options.timeout === undefined ? 300000 : undefined // TODO: better timeout logic + timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic }; let response = await performFireEngineScrape( meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", - request + request, }), request, meta.options.timeout !== undefined ? defaultTimeout + meta.options.waitFor - : Infinity // TODO: better timeout handling + : Infinity, // TODO: better timeout handling ); specialtyScrapeCheck( meta.logger.child({ - method: "scrapeURLWithFireEnginePlaywright/specialtyScrapeCheck" + method: "scrapeURLWithFireEnginePlaywright/specialtyScrapeCheck", }), - response.responseHeaders + response.responseHeaders, ); if (!response.url) { meta.logger.warn("Fire-engine did not return the response's URL", { response, - sourceURL: meta.url + sourceURL: meta.url, }); } @@ -257,14 +257,14 @@ export async function scrapeURLWithFireEnginePlaywright( ...(response.screenshots !== undefined && response.screenshots.length > 0 ? { - screenshot: response.screenshots[0] + screenshot: response.screenshots[0], } - : {}) + : {}), }; } export async function scrapeURLWithFireEngineTLSClient( - meta: Meta + meta: Meta, ): Promise { const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestTLSClient = { @@ -279,29 +279,29 @@ export async function scrapeURLWithFireEngineTLSClient( geolocation: meta.options.geolocation, disableJsDom: meta.internalOptions.v0DisableJsDom, - timeout: meta.options.timeout === undefined ? 300000 : undefined // TODO: better timeout logic + timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic }; let response = await performFireEngineScrape( meta.logger.child({ method: "scrapeURLWithFireEngineChromeCDP/callFireEngine", - request + request, }), request, - meta.options.timeout !== undefined ? defaultTimeout : Infinity // TODO: better timeout handling + meta.options.timeout !== undefined ? defaultTimeout : Infinity, // TODO: better timeout handling ); specialtyScrapeCheck( meta.logger.child({ - method: "scrapeURLWithFireEngineTLSClient/specialtyScrapeCheck" + method: "scrapeURLWithFireEngineTLSClient/specialtyScrapeCheck", }), - response.responseHeaders + response.responseHeaders, ); if (!response.url) { meta.logger.warn("Fire-engine did not return the response's URL", { response, - sourceURL: meta.url + sourceURL: meta.url, }); } @@ -310,6 +310,6 @@ export async function scrapeURLWithFireEngineTLSClient( html: response.content, error: response.pageError, - statusCode: response.pageStatusCode + statusCode: response.pageStatusCode, }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts index ffca4b41..de6ac3f4 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/scrape.ts @@ -58,17 +58,17 @@ export type FireEngineScrapeRequestTLSClient = { const schema = z.object({ jobId: z.string(), - processing: z.boolean() + processing: z.boolean(), }); export async function fireEngineScrape< Engine extends | FireEngineScrapeRequestChromeCDP | FireEngineScrapeRequestPlaywright - | FireEngineScrapeRequestTLSClient + | FireEngineScrapeRequestTLSClient, >( logger: Logger, - request: FireEngineScrapeRequestCommon & Engine + request: FireEngineScrapeRequestCommon & Engine, ): Promise> { const fireEngineURL = process.env.FIRE_ENGINE_BETA_URL!; @@ -78,8 +78,8 @@ export async function fireEngineScrape< { name: "fire-engine: Scrape", attributes: { - url: request.url - } + url: request.url, + }, }, async (span) => { return await robustFetch({ @@ -89,16 +89,16 @@ export async function fireEngineScrape< ...(Sentry.isInitialized() ? { "sentry-trace": Sentry.spanToTraceHeader(span), - baggage: Sentry.spanToBaggageHeader(span) + baggage: Sentry.spanToBaggageHeader(span), } - : {}) + : {}), }, body: request, logger: logger.child({ method: "fireEngineScrape/robustFetch" }), schema, - tryCount: 3 + tryCount: 3, }); - } + }, ); return scrapeRequest; diff --git a/apps/api/src/scraper/scrapeURL/engines/index.ts b/apps/api/src/scraper/scrapeURL/engines/index.ts index 1d9db249..01ac0be9 100644 --- a/apps/api/src/scraper/scrapeURL/engines/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/index.ts @@ -4,7 +4,7 @@ import { scrapeDOCX } from "./docx"; import { scrapeURLWithFireEngineChromeCDP, scrapeURLWithFireEnginePlaywright, - scrapeURLWithFireEngineTLSClient + scrapeURLWithFireEngineTLSClient, } from "./fire-engine"; import { scrapePDF } from "./pdf"; import { scrapeURLWithScrapingBee } from "./scrapingbee"; @@ -43,7 +43,7 @@ export const engines: Engine[] = [ ? [ "fire-engine;chrome-cdp" as const, "fire-engine;playwright" as const, - "fire-engine;tlsclient" as const + "fire-engine;tlsclient" as const, ] : []), ...(useScrapingBee @@ -52,7 +52,7 @@ export const engines: Engine[] = [ ...(usePlaywright ? ["playwright" as const] : []), "fetch", "pdf", - "docx" + "docx", ]; export const featureFlags = [ @@ -66,7 +66,7 @@ export const featureFlags = [ "location", "mobile", "skipTlsVerification", - "useFastMode" + "useFastMode", ] as const; export type FeatureFlag = (typeof featureFlags)[number]; @@ -86,7 +86,7 @@ export const featureFlagOptions: { useFastMode: { priority: 90 }, location: { priority: 10 }, mobile: { priority: 10 }, - skipTlsVerification: { priority: 10 } + skipTlsVerification: { priority: 10 }, } as const; export type EngineScrapeResult = { @@ -116,7 +116,7 @@ const engineHandlers: { playwright: scrapeURLWithPlaywright, fetch: scrapeURLWithFetch, pdf: scrapePDF, - docx: scrapeDOCX + docx: scrapeDOCX, }; export const engineOptions: { @@ -141,9 +141,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: false + useFastMode: false, }, - quality: 1000 // cache should always be tried first + quality: 1000, // cache should always be tried first }, "fire-engine;chrome-cdp": { features: { @@ -157,9 +157,9 @@ export const engineOptions: { location: true, mobile: true, skipTlsVerification: true, - useFastMode: false + useFastMode: false, }, - quality: 50 + quality: 50, }, "fire-engine;playwright": { features: { @@ -173,9 +173,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: false + useFastMode: false, }, - quality: 40 + quality: 40, }, scrapingbee: { features: { @@ -189,9 +189,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: false + useFastMode: false, }, - quality: 30 + quality: 30, }, scrapingbeeLoad: { features: { @@ -205,9 +205,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: false + useFastMode: false, }, - quality: 29 + quality: 29, }, playwright: { features: { @@ -221,9 +221,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: false + useFastMode: false, }, - quality: 20 + quality: 20, }, "fire-engine;tlsclient": { features: { @@ -237,9 +237,9 @@ export const engineOptions: { location: true, mobile: false, skipTlsVerification: false, - useFastMode: true + useFastMode: true, }, - quality: 10 + quality: 10, }, fetch: { features: { @@ -253,9 +253,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: true + useFastMode: true, }, - quality: 5 + quality: 5, }, pdf: { features: { @@ -269,9 +269,9 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: true + useFastMode: true, }, - quality: -10 + quality: -10, }, docx: { features: { @@ -285,10 +285,10 @@ export const engineOptions: { location: false, mobile: false, skipTlsVerification: false, - useFastMode: true + useFastMode: true, }, - quality: -10 - } + quality: -10, + }, }; export function buildFallbackList(meta: Meta): { @@ -297,7 +297,7 @@ export function buildFallbackList(meta: Meta): { }[] { const prioritySum = [...meta.featureFlags].reduce( (a, x) => a + featureFlagOptions[x].priority, - 0 + 0, ); const priorityThreshold = Math.floor(prioritySum / 2); let selectedEngines: { @@ -315,13 +315,13 @@ export function buildFallbackList(meta: Meta): { const supportedFlags = new Set([ ...Object.entries(engineOptions[engine].features) .filter( - ([k, v]) => meta.featureFlags.has(k as FeatureFlag) && v === true + ([k, v]) => meta.featureFlags.has(k as FeatureFlag) && v === true, ) - .map(([k, _]) => k) + .map(([k, _]) => k), ]); const supportScore = [...supportedFlags].reduce( (a, x) => a + featureFlagOptions[x].priority, - 0 + 0, ); const unsupportedFeatures = new Set([...meta.featureFlags]); @@ -338,7 +338,7 @@ export function buildFallbackList(meta: Meta): { prioritySum, priorityThreshold, featureFlags: [...meta.featureFlags], - unsupportedFeatures + unsupportedFeatures, }); } else { meta.logger.debug( @@ -348,22 +348,22 @@ export function buildFallbackList(meta: Meta): { prioritySum, priorityThreshold, featureFlags: [...meta.featureFlags], - unsupportedFeatures - } + unsupportedFeatures, + }, ); } } if (selectedEngines.some((x) => engineOptions[x.engine].quality > 0)) { selectedEngines = selectedEngines.filter( - (x) => engineOptions[x.engine].quality > 0 + (x) => engineOptions[x.engine].quality > 0, ); } selectedEngines.sort( (a, b) => b.supportScore - a.supportScore || - engineOptions[b.engine].quality - engineOptions[a.engine].quality + engineOptions[b.engine].quality - engineOptions[a.engine].quality, ); return selectedEngines; @@ -371,16 +371,16 @@ export function buildFallbackList(meta: Meta): { export async function scrapeURLWithEngine( meta: Meta, - engine: Engine + engine: Engine, ): Promise { const fn = engineHandlers[engine]; const logger = meta.logger.child({ method: fn.name ?? "scrapeURLWithEngine", - engine + engine, }); const _meta = { ...meta, - logger + logger, }; return await fn(_meta); diff --git a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts index 62313a71..341a4f1a 100644 --- a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts @@ -14,10 +14,10 @@ type PDFProcessorResult = { html: string; markdown?: string }; async function scrapePDFWithLlamaParse( meta: Meta, - tempFilePath: string + tempFilePath: string, ): Promise { meta.logger.debug("Processing PDF document with LlamaIndex", { - tempFilePath + tempFilePath, }); const uploadForm = new FormData(); @@ -28,7 +28,7 @@ async function scrapePDFWithLlamaParse( name: tempFilePath, stream() { return createReadStream( - tempFilePath + tempFilePath, ) as unknown as ReadableStream; }, arrayBuffer() { @@ -41,22 +41,22 @@ async function scrapePDFWithLlamaParse( slice(start, end, contentType) { throw Error("Unimplemented in mock Blob: slice"); }, - type: "application/pdf" + type: "application/pdf", } as Blob); const upload = await robustFetch({ url: "https://api.cloud.llamaindex.ai/api/parsing/upload", method: "POST", headers: { - Authorization: `Bearer ${process.env.LLAMAPARSE_API_KEY}` + Authorization: `Bearer ${process.env.LLAMAPARSE_API_KEY}`, }, body: uploadForm, logger: meta.logger.child({ - method: "scrapePDFWithLlamaParse/upload/robustFetch" + method: "scrapePDFWithLlamaParse/upload/robustFetch", }), schema: z.object({ - id: z.string() - }) + id: z.string(), + }), }); const jobId = upload.id; @@ -70,18 +70,18 @@ async function scrapePDFWithLlamaParse( url: `https://api.cloud.llamaindex.ai/api/parsing/job/${jobId}/result/markdown`, method: "GET", headers: { - Authorization: `Bearer ${process.env.LLAMAPARSE_API_KEY}` + Authorization: `Bearer ${process.env.LLAMAPARSE_API_KEY}`, }, logger: meta.logger.child({ - method: "scrapePDFWithLlamaParse/result/robustFetch" + method: "scrapePDFWithLlamaParse/result/robustFetch", }), schema: z.object({ - markdown: z.string() - }) + markdown: z.string(), + }), }); return { markdown: result.markdown, - html: await marked.parse(result.markdown, { async: true }) + html: await marked.parse(result.markdown, { async: true }), }; } catch (e) { if (e instanceof Error && e.message === "Request sent failure status") { @@ -93,7 +93,7 @@ async function scrapePDFWithLlamaParse( throw new RemoveFeatureError(["pdf"]); } else { throw new Error("LlamaParse threw an error", { - cause: e.cause + cause: e.cause, }); } } else { @@ -109,7 +109,7 @@ async function scrapePDFWithLlamaParse( async function scrapePDFWithParsePDF( meta: Meta, - tempFilePath: string + tempFilePath: string, ): Promise { meta.logger.debug("Processing PDF document with parse-pdf", { tempFilePath }); @@ -118,7 +118,7 @@ async function scrapePDFWithParsePDF( return { markdown: escaped, - html: escaped + html: escaped, }; } @@ -131,7 +131,7 @@ export async function scrapePDF(meta: Meta): Promise { statusCode: file.response.status, html: content, - markdown: content + markdown: content, }; } @@ -144,22 +144,22 @@ export async function scrapePDF(meta: Meta): Promise { { ...meta, logger: meta.logger.child({ - method: "scrapePDF/scrapePDFWithLlamaParse" - }) + method: "scrapePDF/scrapePDFWithLlamaParse", + }), }, - tempFilePath + tempFilePath, ); } catch (error) { if (error instanceof Error && error.message === "LlamaParse timed out") { meta.logger.warn("LlamaParse timed out -- falling back to parse-pdf", { - error + error, }); } else if (error instanceof RemoveFeatureError) { throw error; } else { meta.logger.warn( "LlamaParse failed to parse PDF -- falling back to parse-pdf", - { error } + { error }, ); Sentry.captureException(error); } @@ -170,9 +170,11 @@ export async function scrapePDF(meta: Meta): Promise { result = await scrapePDFWithParsePDF( { ...meta, - logger: meta.logger.child({ method: "scrapePDF/scrapePDFWithParsePDF" }) + logger: meta.logger.child({ + method: "scrapePDF/scrapePDFWithParsePDF", + }), }, - tempFilePath + tempFilePath, ); } @@ -183,6 +185,6 @@ export async function scrapePDF(meta: Meta): Promise { statusCode: response.status, html: result.html, - markdown: result.markdown + markdown: result.markdown, }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts b/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts index a8c16045..c92b1d90 100644 --- a/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts @@ -5,7 +5,7 @@ import { TimeoutError } from "../../error"; import { robustFetch } from "../../lib/fetch"; export async function scrapeURLWithPlaywright( - meta: Meta + meta: Meta, ): Promise { const timeout = 20000 + meta.options.waitFor; @@ -13,35 +13,35 @@ export async function scrapeURLWithPlaywright( await robustFetch({ url: process.env.PLAYWRIGHT_MICROSERVICE_URL!, headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: { url: meta.url, wait_after_load: meta.options.waitFor, timeout, - headers: meta.options.headers + headers: meta.options.headers, }, method: "POST", logger: meta.logger.child("scrapeURLWithPlaywright/robustFetch"), schema: z.object({ content: z.string(), pageStatusCode: z.number(), - pageError: z.string().optional() - }) + pageError: z.string().optional(), + }), }), (async () => { await new Promise((resolve) => setTimeout(() => resolve(null), 20000)); throw new TimeoutError( "Playwright was unable to scrape the page before timing out", - { cause: { timeout } } + { cause: { timeout } }, ); - })() + })(), ]); return { url: meta.url, // TODO: impove redirect following html: response.content, statusCode: response.pageStatusCode, - error: response.pageError + error: response.pageError, }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts b/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts index 8388016a..50ac502b 100644 --- a/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts @@ -8,7 +8,7 @@ import { EngineError } from "../../error"; const client = new ScrapingBeeClient(process.env.SCRAPING_BEE_API_KEY!); export function scrapeURLWithScrapingBee( - wait_browser: "domcontentloaded" | "networkidle2" + wait_browser: "domcontentloaded" | "networkidle2", ): (meta: Meta) => Promise { return async (meta: Meta): Promise => { let response: AxiosResponse; @@ -23,12 +23,12 @@ export function scrapeURLWithScrapingBee( json_response: true, screenshot: meta.options.formats.includes("screenshot"), screenshot_full_page: meta.options.formats.includes( - "screenshot@fullPage" - ) + "screenshot@fullPage", + ), }, headers: { - "ScrapingService-Request": "TRUE" // this is sent to the page, not to ScrapingBee - mogery - } + "ScrapingService-Request": "TRUE", // this is sent to the page, not to ScrapingBee - mogery + }, }); } catch (error) { if (error instanceof AxiosError && error.response !== undefined) { @@ -51,25 +51,25 @@ export function scrapeURLWithScrapingBee( if (body.errors || body.body?.error || isHiddenEngineError) { meta.logger.error("ScrapingBee threw an error", { - body: body.body?.error ?? body.errors ?? body.body ?? body + body: body.body?.error ?? body.errors ?? body.body ?? body, }); throw new EngineError("Engine error #34", { - cause: { body, statusCode: response.status } + cause: { body, statusCode: response.status }, }); } if (typeof body.body !== "string") { meta.logger.error("ScrapingBee: Body is not string??", { body }); throw new EngineError("Engine error #35", { - cause: { body, statusCode: response.status } + cause: { body, statusCode: response.status }, }); } specialtyScrapeCheck( meta.logger.child({ - method: "scrapeURLWithScrapingBee/specialtyScrapeCheck" + method: "scrapeURLWithScrapingBee/specialtyScrapeCheck", }), - body.headers + body.headers, ); return { @@ -80,9 +80,9 @@ export function scrapeURLWithScrapingBee( statusCode: response.status, ...(body.screenshot ? { - screenshot: `data:image/png;base64,${body.screenshot}` + screenshot: `data:image/png;base64,${body.screenshot}`, } - : {}) + : {}), }; }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts b/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts index 84a52425..e2e3ee6f 100644 --- a/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts +++ b/apps/api/src/scraper/scrapeURL/engines/utils/downloadFile.ts @@ -13,13 +13,13 @@ export async function fetchFileToBuffer(url: string): Promise<{ const response = await fetch(url); // TODO: maybe we could use tlsclient for this? for proxying return { response, - buffer: Buffer.from(await response.arrayBuffer()) + buffer: Buffer.from(await response.arrayBuffer()), }; } export async function downloadFile( id: string, - url: string + url: string, ): Promise<{ response: undici.Response; tempFilePath: string; @@ -32,9 +32,9 @@ export async function downloadFile( const response = await undici.fetch(url, { dispatcher: new undici.Agent({ connect: { - rejectUnauthorized: false - } - }) + rejectUnauthorized: false, + }, + }), }); // This should never happen in the current state of JS (2024), but let's check anyways. @@ -47,13 +47,13 @@ export async function downloadFile( tempFileWrite.on("finish", () => resolve(null)); tempFileWrite.on("error", (error) => { reject( - new EngineError("Failed to write to temp file", { cause: { error } }) + new EngineError("Failed to write to temp file", { cause: { error } }), ); }); }); return { response, - tempFilePath + tempFilePath, }; } diff --git a/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts b/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts index 4f497e52..352f6a7e 100644 --- a/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts +++ b/apps/api/src/scraper/scrapeURL/engines/utils/specialtyHandler.ts @@ -3,15 +3,15 @@ import { AddFeatureError } from "../../error"; export function specialtyScrapeCheck( logger: Logger, - headers: Record | undefined + headers: Record | undefined, ) { const contentType = (Object.entries(headers ?? {}).find( - (x) => x[0].toLowerCase() === "content-type" + (x) => x[0].toLowerCase() === "content-type", ) ?? [])[1]; if (contentType === undefined) { logger.warn("Failed to check contentType -- was not present in headers", { - headers + headers, }); } else if ( contentType === "application/pdf" || @@ -23,7 +23,7 @@ export function specialtyScrapeCheck( contentType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || contentType.startsWith( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document;" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document;", ) ) { // .docx diff --git a/apps/api/src/scraper/scrapeURL/error.ts b/apps/api/src/scraper/scrapeURL/error.ts index c6eb45e3..ec044745 100644 --- a/apps/api/src/scraper/scrapeURL/error.ts +++ b/apps/api/src/scraper/scrapeURL/error.ts @@ -19,7 +19,7 @@ export class NoEnginesLeftError extends Error { constructor(fallbackList: Engine[], results: EngineResultsTracker) { super( - "All scraping engines failed! -- Double check the URL to make sure it's not broken. If the issue persists, contact us at help@firecrawl.com." + "All scraping engines failed! -- Double check the URL to make sure it's not broken. If the issue persists, contact us at help@firecrawl.com.", ); this.fallbackList = fallbackList; this.results = results; @@ -40,7 +40,8 @@ export class RemoveFeatureError extends Error { constructor(featureFlags: FeatureFlag[]) { super( - "Incorrect feature flags have been discovered: " + featureFlags.join(", ") + "Incorrect feature flags have been discovered: " + + featureFlags.join(", "), ); this.featureFlags = featureFlags; } @@ -50,7 +51,7 @@ export class SiteError extends Error { public code: string; constructor(code: string) { super( - "Specified URL is failing to load in the browser. Error code: " + code + "Specified URL is failing to load in the browser. Error code: " + code, ); this.code = code; } diff --git a/apps/api/src/scraper/scrapeURL/index.ts b/apps/api/src/scraper/scrapeURL/index.ts index 0a0b6c92..a3eb6f1e 100644 --- a/apps/api/src/scraper/scrapeURL/index.ts +++ b/apps/api/src/scraper/scrapeURL/index.ts @@ -8,7 +8,7 @@ import { Engine, EngineScrapeResult, FeatureFlag, - scrapeURLWithEngine + scrapeURLWithEngine, } from "./engines"; import { parseMarkdown } from "../../lib/html-to-markdown"; import { @@ -17,7 +17,7 @@ import { NoEnginesLeftError, RemoveFeatureError, SiteError, - TimeoutError + TimeoutError, } from "./error"; import { executeTransformers } from "./transformers"; import { LLMRefusalError } from "./transformers/llmExtract"; @@ -50,7 +50,7 @@ export type Meta = { function buildFeatureFlags( url: string, options: ScrapeOptions, - internalOptions: InternalOptions + internalOptions: InternalOptions, ): Set { const flags: Set = new Set(); @@ -112,7 +112,7 @@ function buildMetaObject( id: string, url: string, options: ScrapeOptions, - internalOptions: InternalOptions + internalOptions: InternalOptions, ): Meta { const specParams = urlSpecificParams[new URL(url).hostname.replace(/^www\./, "")]; @@ -120,14 +120,14 @@ function buildMetaObject( options = Object.assign(options, specParams.scrapeOptions); internalOptions = Object.assign( internalOptions, - specParams.internalOptions + specParams.internalOptions, ); } const _logger = logger.child({ module: "ScrapeURL", scrapeId: id, - scrapeURL: url + scrapeURL: url, }); const logs: any[] = []; @@ -138,7 +138,7 @@ function buildMetaObject( internalOptions, logger: _logger, logs, - featureFlags: buildFeatureFlags(url, options, internalOptions) + featureFlags: buildFeatureFlags(url, options, internalOptions), }; } @@ -229,7 +229,7 @@ async function scrapeURLLoop(meta: Meta): Promise { factors: { isLongEnough, isGoodStatusCode, hasNoPageError }, unsupportedFeatures, startedAt, - finishedAt: Date.now() + finishedAt: Date.now(), }; // NOTE: TODO: what to do when status code is bad is tough... @@ -237,35 +237,35 @@ async function scrapeURLLoop(meta: Meta): Promise { // should we just use all the fallbacks and pick the one with the longest text? - mogery if (isLongEnough || !isGoodStatusCode) { meta.logger.info("Scrape via " + engine + " deemed successful.", { - factors: { isLongEnough, isGoodStatusCode, hasNoPageError } + factors: { isLongEnough, isGoodStatusCode, hasNoPageError }, }); result = { engine, unsupportedFeatures, - result: engineResult as EngineScrapeResult & { markdown: string } + result: engineResult as EngineScrapeResult & { markdown: string }, }; break; } } catch (error) { if (error instanceof EngineError) { meta.logger.info("Engine " + engine + " could not scrape the page.", { - error + error, }); results[engine] = { state: "error", error: safeguardCircularError(error), unexpected: false, startedAt, - finishedAt: Date.now() + finishedAt: Date.now(), }; } else if (error instanceof TimeoutError) { meta.logger.info("Engine " + engine + " timed out while scraping.", { - error + error, }); results[engine] = { state: "timeout", startedAt, - finishedAt: Date.now() + finishedAt: Date.now(), }; } else if ( error instanceof AddFeatureError || @@ -278,7 +278,7 @@ async function scrapeURLLoop(meta: Meta): Promise { error: safeguardCircularError(error), unexpected: true, startedAt, - finishedAt: Date.now() + finishedAt: Date.now(), }; error.results = results; meta.logger.warn("LLM refusal encountered", { error }); @@ -289,14 +289,14 @@ async function scrapeURLLoop(meta: Meta): Promise { Sentry.captureException(error); meta.logger.info( "An unexpected error happened while scraping with " + engine + ".", - { error } + { error }, ); results[engine] = { state: "error", error: safeguardCircularError(error), unexpected: true, startedAt, - finishedAt: Date.now() + finishedAt: Date.now(), }; } } @@ -305,7 +305,7 @@ async function scrapeURLLoop(meta: Meta): Promise { if (result === null) { throw new NoEnginesLeftError( fallbackList.map((x) => x.engine), - results + results, ); } @@ -318,15 +318,15 @@ async function scrapeURLLoop(meta: Meta): Promise { sourceURL: meta.url, url: result.result.url, statusCode: result.result.statusCode, - error: result.result.error - } + error: result.result.error, + }, }; if (result.unsupportedFeatures.size > 0) { const warning = `The engine used does not support the following features: ${[...result.unsupportedFeatures].join(", ")} -- your scrape may be partial.`; meta.logger.warn(warning, { engine: result.engine, - unsupportedFeatures: result.unsupportedFeatures + unsupportedFeatures: result.unsupportedFeatures, }); document.warning = document.warning !== undefined @@ -340,7 +340,7 @@ async function scrapeURLLoop(meta: Meta): Promise { success: true, document, logs: meta.logs, - engines: results + engines: results, }; } @@ -348,7 +348,7 @@ export async function scrapeURL( id: string, url: string, options: ScrapeOptions, - internalOptions: InternalOptions = {} + internalOptions: InternalOptions = {}, ): Promise { const meta = buildMetaObject(id, url, options, internalOptions); try { @@ -363,10 +363,10 @@ export async function scrapeURL( meta.logger.debug( "More feature flags requested by scraper: adding " + error.featureFlags.join(", "), - { error, existingFlags: meta.featureFlags } + { error, existingFlags: meta.featureFlags }, ); meta.featureFlags = new Set( - [...meta.featureFlags].concat(error.featureFlags) + [...meta.featureFlags].concat(error.featureFlags), ); } else if ( error instanceof RemoveFeatureError && @@ -375,12 +375,12 @@ export async function scrapeURL( meta.logger.debug( "Incorrect feature flags reported by scraper: removing " + error.featureFlags.join(","), - { error, existingFlags: meta.featureFlags } + { error, existingFlags: meta.featureFlags }, ); meta.featureFlags = new Set( [...meta.featureFlags].filter( - (x) => !error.featureFlags.includes(x) - ) + (x) => !error.featureFlags.includes(x), + ), ); } else { throw error; @@ -415,7 +415,7 @@ export async function scrapeURL( success: false, error, logs: meta.logs, - engines: results + engines: results, }; } } diff --git a/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts b/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts index 6d71c036..7d612875 100644 --- a/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts +++ b/apps/api/src/scraper/scrapeURL/lib/extractLinks.ts @@ -27,7 +27,7 @@ export function extractLinks(html: string, baseUrl: string): string[] { } catch (error) { logger.error( `Failed to construct URL for href: ${href} with base: ${baseUrl}`, - { error } + { error }, ); } } diff --git a/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts b/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts index 0f581373..040bf0ee 100644 --- a/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts +++ b/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts @@ -4,7 +4,7 @@ import { Meta } from ".."; export function extractMetadata( meta: Meta, - html: string + html: string, ): Document["metadata"] { let title: string | undefined = undefined; let description: string | undefined = undefined; @@ -148,6 +148,6 @@ export function extractMetadata( publishedTime, articleTag, articleSection, - ...customMetadata + ...customMetadata, }; } diff --git a/apps/api/src/scraper/scrapeURL/lib/fetch.ts b/apps/api/src/scraper/scrapeURL/lib/fetch.ts index 400c23a7..897587a9 100644 --- a/apps/api/src/scraper/scrapeURL/lib/fetch.ts +++ b/apps/api/src/scraper/scrapeURL/lib/fetch.ts @@ -20,7 +20,7 @@ export type RobustFetchParams> = { export async function robustFetch< Schema extends z.Schema, - Output = z.infer + Output = z.infer, >({ url, logger, @@ -32,7 +32,7 @@ export async function robustFetch< ignoreFailure = false, requestId = uuid(), tryCount = 1, - tryCooldown + tryCooldown, }: RobustFetchParams): Promise { const params = { url, @@ -44,7 +44,7 @@ export async function robustFetch< ignoreResponse, ignoreFailure, tryCount, - tryCooldown + tryCooldown, }; let request: Response; @@ -56,20 +56,20 @@ export async function robustFetch< ? {} : body !== undefined ? { - "Content-Type": "application/json" + "Content-Type": "application/json", } : {}), - ...(headers !== undefined ? headers : {}) + ...(headers !== undefined ? headers : {}), }, ...(body instanceof FormData ? { - body + body, } : body !== undefined ? { - body: JSON.stringify(body) + body: JSON.stringify(body), } - : {}) + : {}), }); } catch (error) { if (!ignoreFailure) { @@ -77,12 +77,12 @@ export async function robustFetch< if (tryCount > 1) { logger.debug( "Request failed, trying " + (tryCount - 1) + " more times", - { params, error, requestId } + { params, error, requestId }, ); return await robustFetch({ ...params, requestId, - tryCount: tryCount - 1 + tryCount: tryCount - 1, }); } else { logger.debug("Request failed", { params, error, requestId }); @@ -90,8 +90,8 @@ export async function robustFetch< cause: { params, requestId, - error - } + error, + }, }); } } else { @@ -106,39 +106,39 @@ export async function robustFetch< const response = { status: request.status, headers: request.headers, - body: await request.text() // NOTE: can this throw an exception? + body: await request.text(), // NOTE: can this throw an exception? }; if (request.status >= 300) { if (tryCount > 1) { logger.debug( "Request sent failure status, trying " + (tryCount - 1) + " more times", - { params, request, response, requestId } + { params, request, response, requestId }, ); if (tryCooldown !== undefined) { await new Promise((resolve) => - setTimeout(() => resolve(null), tryCooldown) + setTimeout(() => resolve(null), tryCooldown), ); } return await robustFetch({ ...params, requestId, - tryCount: tryCount - 1 + tryCount: tryCount - 1, }); } else { logger.debug("Request sent failure status", { params, request, response, - requestId + requestId, }); throw new Error("Request sent failure status", { cause: { params, request, response, - requestId - } + requestId, + }, }); } } @@ -151,15 +151,15 @@ export async function robustFetch< params, request, response, - requestId + requestId, }); throw new Error("Request sent malformed JSON", { cause: { params, request, response, - requestId - } + requestId, + }, }); } @@ -174,7 +174,7 @@ export async function robustFetch< response, requestId, error, - schema + schema, }); throw new Error("Response does not match provided schema", { cause: { @@ -183,8 +183,8 @@ export async function robustFetch< response, requestId, error, - schema - } + schema, + }, }); } else { logger.debug("Parsing response with provided schema failed", { @@ -193,7 +193,7 @@ export async function robustFetch< response, requestId, error, - schema + schema, }); throw new Error("Parsing response with provided schema failed", { cause: { @@ -202,8 +202,8 @@ export async function robustFetch< response, requestId, error, - schema - } + schema, + }, }); } } diff --git a/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts b/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts index 7701aeaf..3afbabd5 100644 --- a/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts +++ b/apps/api/src/scraper/scrapeURL/lib/removeUnwantedElements.ts @@ -47,14 +47,14 @@ const excludeNonMainTags = [ ".widget", "#widget", ".cookie", - "#cookie" + "#cookie", ]; const forceIncludeMainTags = ["#main"]; export const removeUnwantedElements = ( html: string, - scrapeOptions: ScrapeOptions + scrapeOptions: ScrapeOptions, ) => { const soup = load(html); @@ -89,11 +89,11 @@ export const removeUnwantedElements = ( const attributes = element.attribs; const tagNameMatches = regexPattern.test(element.name); const attributesMatch = Object.keys(attributes).some((attr) => - regexPattern.test(`${attr}="${attributes[attr]}"`) + regexPattern.test(`${attr}="${attributes[attr]}"`), ); if (tag.startsWith("*.")) { classMatch = Object.keys(attributes).some((attr) => - regexPattern.test(`class="${attributes[attr]}"`) + regexPattern.test(`class="${attributes[attr]}"`), ); } return tagNameMatches || attributesMatch || classMatch; @@ -110,7 +110,7 @@ export const removeUnwantedElements = ( if (scrapeOptions.onlyMainContent) { excludeNonMainTags.forEach((tag) => { const elementsToRemove = soup(tag).filter( - forceIncludeMainTags.map((x) => ":not(:has(" + x + "))").join("") + forceIncludeMainTags.map((x) => ":not(:has(" + x + "))").join(""), ); elementsToRemove.remove(); diff --git a/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts b/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts index 0810dc93..8a3d6c3e 100644 --- a/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts +++ b/apps/api/src/scraper/scrapeURL/lib/urlSpecificParams.ts @@ -42,10 +42,10 @@ export const urlSpecificParams: Record = { // }, "digikey.com": { scrapeOptions: {}, - internalOptions: { forceEngine: "fire-engine;tlsclient" } + internalOptions: { forceEngine: "fire-engine;tlsclient" }, }, "lorealparis.hu": { scrapeOptions: {}, - internalOptions: { forceEngine: "fire-engine;tlsclient" } - } + internalOptions: { forceEngine: "fire-engine;tlsclient" }, + }, }; diff --git a/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts b/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts index 8bef0c2c..8b783821 100644 --- a/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts +++ b/apps/api/src/scraper/scrapeURL/scrapeURL.test.ts @@ -13,7 +13,7 @@ const testEngines: (Engine | undefined)[] = [ "fire-engine;tlsclient", "scrapingbee", "scrapingbeeLoad", - "fetch" + "fetch", ]; const testEnginesScreenshot: (Engine | undefined)[] = [ @@ -21,7 +21,7 @@ const testEnginesScreenshot: (Engine | undefined)[] = [ "fire-engine;chrome-cdp", "fire-engine;playwright", "scrapingbee", - "scrapingbeeLoad" + "scrapingbeeLoad", ]; describe("Standalone scrapeURL tests", () => { @@ -31,7 +31,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-basic", "https://www.roastmywebsite.ai/", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -46,26 +46,26 @@ describe("Standalone scrapeURL tests", () => { expect(out.document.metadata.error).toBeUndefined(); expect(out.document.metadata.title).toBe("Roast My Website"); expect(out.document.metadata.description).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(out.document.metadata.keywords).toBe( - "Roast My Website,Roast,Website,GitHub,Firecrawl" + "Roast My Website,Roast,Website,GitHub,Firecrawl", ); expect(out.document.metadata.robots).toBe("follow, index"); expect(out.document.metadata.ogTitle).toBe("Roast My Website"); expect(out.document.metadata.ogDescription).toBe( - "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️" + "Welcome to Roast My Website, the ultimate tool for putting your website through the wringer! This repository harnesses the power of Firecrawl to scrape and capture screenshots of websites, and then unleashes the latest LLM vision models to mercilessly roast them. 🌶️", ); expect(out.document.metadata.ogUrl).toBe( - "https://www.roastmywebsite.ai" + "https://www.roastmywebsite.ai", ); expect(out.document.metadata.ogImage).toBe( - "https://www.roastmywebsite.ai/og.png" + "https://www.roastmywebsite.ai/og.png", ); expect(out.document.metadata.ogLocaleAlternate).toStrictEqual([]); expect(out.document.metadata.ogSiteName).toBe("Roast My Website"); expect(out.document.metadata.sourceURL).toBe( - "https://www.roastmywebsite.ai/" + "https://www.roastmywebsite.ai/", ); expect(out.document.metadata.statusCode).toBe(200); } @@ -76,9 +76,9 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-formats-markdown-html", "https://roastmywebsite.ai", scrapeOptions.parse({ - formats: ["markdown", "html"] + formats: ["markdown", "html"], }), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -100,9 +100,9 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-onlyMainContent-false", "https://www.scrapethissite.com/", scrapeOptions.parse({ - onlyMainContent: false + onlyMainContent: false, }), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -123,9 +123,9 @@ describe("Standalone scrapeURL tests", () => { "https://www.scrapethissite.com/", scrapeOptions.parse({ onlyMainContent: false, - excludeTags: [".nav", "#footer", "strong"] + excludeTags: [".nav", "#footer", "strong"], }), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -145,7 +145,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-400", "https://httpstat.us/400", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -163,7 +163,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-401", "https://httpstat.us/401", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -181,7 +181,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-403", "https://httpstat.us/403", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -199,7 +199,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-404", "https://httpstat.us/404", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -217,7 +217,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-405", "https://httpstat.us/405", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -235,7 +235,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-500", "https://httpstat.us/500", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -253,7 +253,7 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-redirect", "https://scrapethissite.com/", scrapeOptions.parse({}), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -264,10 +264,10 @@ describe("Standalone scrapeURL tests", () => { expect(out.document.markdown).toContain("Explore Sandbox"); expect(out.document).toHaveProperty("metadata"); expect(out.document.metadata.sourceURL).toBe( - "https://scrapethissite.com/" + "https://scrapethissite.com/", ); expect(out.document.metadata.url).toBe( - "https://www.scrapethissite.com/" + "https://www.scrapethissite.com/", ); expect(out.document.metadata.statusCode).toBe(200); expect(out.document.metadata.error).toBeUndefined(); @@ -283,9 +283,9 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-screenshot", "https://www.scrapethissite.com/", scrapeOptions.parse({ - formats: ["screenshot"] + formats: ["screenshot"], }), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -296,8 +296,8 @@ describe("Standalone scrapeURL tests", () => { expect(typeof out.document.screenshot).toBe("string"); expect( out.document.screenshot!.startsWith( - "https://service.firecrawl.dev/storage/v1/object/public/media/" - ) + "https://service.firecrawl.dev/storage/v1/object/public/media/", + ), ); // TODO: attempt to fetch screenshot expect(out.document).toHaveProperty("metadata"); @@ -311,9 +311,9 @@ describe("Standalone scrapeURL tests", () => { "test:scrape-screenshot-fullPage", "https://www.scrapethissite.com/", scrapeOptions.parse({ - formats: ["screenshot@fullPage"] + formats: ["screenshot@fullPage"], }), - { forceEngine } + { forceEngine }, ); // expect(out.logs.length).toBeGreaterThan(0); @@ -324,8 +324,8 @@ describe("Standalone scrapeURL tests", () => { expect(typeof out.document.screenshot).toBe("string"); expect( out.document.screenshot!.startsWith( - "https://service.firecrawl.dev/storage/v1/object/public/media/" - ) + "https://service.firecrawl.dev/storage/v1/object/public/media/", + ), ); // TODO: attempt to fetch screenshot expect(out.document).toHaveProperty("metadata"); @@ -333,14 +333,14 @@ describe("Standalone scrapeURL tests", () => { expect(out.document.metadata.error).toBeUndefined(); } }, 30000); - } + }, ); it("Scrape of a PDF file", async () => { const out = await scrapeURL( "test:scrape-pdf", "https://arxiv.org/pdf/astro-ph/9301001.pdf", - scrapeOptions.parse({}) + scrapeOptions.parse({}), ); // expect(out.logs.length).toBeGreaterThan(0); @@ -358,7 +358,7 @@ describe("Standalone scrapeURL tests", () => { const out = await scrapeURL( "test:scrape-docx", "https://nvca.org/wp-content/uploads/2019/06/NVCA-Model-Document-Stock-Purchase-Agreement.docx", - scrapeOptions.parse({}) + scrapeOptions.parse({}), ); // expect(out.logs.length).toBeGreaterThan(0); @@ -367,7 +367,7 @@ describe("Standalone scrapeURL tests", () => { expect(out.document.warning).toBeUndefined(); expect(out.document).toHaveProperty("metadata"); expect(out.document.markdown).toContain( - "SERIES A PREFERRED STOCK PURCHASE AGREEMENT" + "SERIES A PREFERRED STOCK PURCHASE AGREEMENT", ); expect(out.document.metadata.statusCode).toBe(200); expect(out.document.metadata.error).toBeUndefined(); @@ -388,13 +388,13 @@ describe("Standalone scrapeURL tests", () => { properties: { company_mission: { type: "string" }, supports_sso: { type: "boolean" }, - is_open_source: { type: "boolean" } + is_open_source: { type: "boolean" }, }, required: ["company_mission", "supports_sso", "is_open_source"], - additionalProperties: false - } - } - }) + additionalProperties: false, + }, + }, + }), ); // expect(out.logs.length).toBeGreaterThan(0); @@ -423,13 +423,13 @@ describe("Standalone scrapeURL tests", () => { properties: { company_mission: { type: "string" }, supports_sso: { type: "boolean" }, - is_open_source: { type: "boolean" } + is_open_source: { type: "boolean" }, }, required: ["company_mission", "supports_sso", "is_open_source"], - additionalProperties: false - } - } - }) + additionalProperties: false, + }, + }, + }), ); // expect(out.logs.length).toBeGreaterThan(0); @@ -460,7 +460,7 @@ describe("Standalone scrapeURL tests", () => { message: value.message, name: value.name, cause: value.cause, - stack: value.stack + stack: value.stack, }; } else { return value; @@ -486,6 +486,6 @@ describe("Standalone scrapeURL tests", () => { expect(out.document.metadata.statusCode).toBe(200); } }, - 30000 + 30000, ); }); diff --git a/apps/api/src/scraper/scrapeURL/transformers/cache.ts b/apps/api/src/scraper/scrapeURL/transformers/cache.ts index 4a31da1f..523a8419 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/cache.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/cache.ts @@ -11,7 +11,7 @@ export function saveToCache(meta: Meta, document: Document): Document { if (document.rawHtml === undefined) { throw new Error( - "rawHtml is undefined -- this transformer is being called out of order" + "rawHtml is undefined -- this transformer is being called out of order", ); } @@ -22,7 +22,7 @@ export function saveToCache(meta: Meta, document: Document): Document { html: document.rawHtml!, statusCode: document.metadata.statusCode!, url: document.metadata.url ?? document.metadata.sourceURL!, - error: document.metadata.error ?? undefined + error: document.metadata.error ?? undefined, }; saveEntryToCache(key, entry); diff --git a/apps/api/src/scraper/scrapeURL/transformers/index.ts b/apps/api/src/scraper/scrapeURL/transformers/index.ts index 5afceda2..e14896ef 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/index.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/index.ts @@ -11,33 +11,33 @@ import { saveToCache } from "./cache"; export type Transformer = ( meta: Meta, - document: Document + document: Document, ) => Document | Promise; export function deriveMetadataFromRawHTML( meta: Meta, - document: Document + document: Document, ): Document { if (document.rawHtml === undefined) { throw new Error( - "rawHtml is undefined -- this transformer is being called out of order" + "rawHtml is undefined -- this transformer is being called out of order", ); } document.metadata = { ...extractMetadata(meta, document.rawHtml), - ...document.metadata + ...document.metadata, }; return document; } export function deriveHTMLFromRawHTML( meta: Meta, - document: Document + document: Document, ): Document { if (document.rawHtml === undefined) { throw new Error( - "rawHtml is undefined -- this transformer is being called out of order" + "rawHtml is undefined -- this transformer is being called out of order", ); } @@ -47,11 +47,11 @@ export function deriveHTMLFromRawHTML( export async function deriveMarkdownFromHTML( _meta: Meta, - document: Document + document: Document, ): Promise { if (document.html === undefined) { throw new Error( - "html is undefined -- this transformer is being called out of order" + "html is undefined -- this transformer is being called out of order", ); } @@ -64,7 +64,7 @@ export function deriveLinksFromHTML(meta: Meta, document: Document): Document { if (meta.options.formats.includes("links")) { if (document.html === undefined) { throw new Error( - "html is undefined -- this transformer is being called out of order" + "html is undefined -- this transformer is being called out of order", ); } @@ -76,7 +76,7 @@ export function deriveLinksFromHTML(meta: Meta, document: Document): Document { export function coerceFieldsToFormats( meta: Meta, - document: Document + document: Document, ): Document { const formats = new Set(meta.options.formats); @@ -84,7 +84,7 @@ export function coerceFieldsToFormats( delete document.markdown; } else if (formats.has("markdown") && document.markdown === undefined) { meta.logger.warn( - "Request had format: markdown, but there was no markdown field in the result." + "Request had format: markdown, but there was no markdown field in the result.", ); } @@ -92,7 +92,7 @@ export function coerceFieldsToFormats( delete document.rawHtml; } else if (formats.has("rawHtml") && document.rawHtml === undefined) { meta.logger.warn( - "Request had format: rawHtml, but there was no rawHtml field in the result." + "Request had format: rawHtml, but there was no rawHtml field in the result.", ); } @@ -100,7 +100,7 @@ export function coerceFieldsToFormats( delete document.html; } else if (formats.has("html") && document.html === undefined) { meta.logger.warn( - "Request had format: html, but there was no html field in the result." + "Request had format: html, but there was no html field in the result.", ); } @@ -110,7 +110,7 @@ export function coerceFieldsToFormats( document.screenshot !== undefined ) { meta.logger.warn( - "Removed screenshot from Document because it wasn't in formats -- this is very wasteful and indicates a bug." + "Removed screenshot from Document because it wasn't in formats -- this is very wasteful and indicates a bug.", ); delete document.screenshot; } else if ( @@ -118,29 +118,29 @@ export function coerceFieldsToFormats( document.screenshot === undefined ) { meta.logger.warn( - "Request had format: screenshot / screenshot@fullPage, but there was no screenshot field in the result." + "Request had format: screenshot / screenshot@fullPage, but there was no screenshot field in the result.", ); } if (!formats.has("links") && document.links !== undefined) { meta.logger.warn( - "Removed links from Document because it wasn't in formats -- this is wasteful and indicates a bug." + "Removed links from Document because it wasn't in formats -- this is wasteful and indicates a bug.", ); delete document.links; } else if (formats.has("links") && document.links === undefined) { meta.logger.warn( - "Request had format: links, but there was no links field in the result." + "Request had format: links, but there was no links field in the result.", ); } if (!formats.has("extract") && document.extract !== undefined) { meta.logger.warn( - "Removed extract from Document because it wasn't in formats -- this is extremely wasteful and indicates a bug." + "Removed extract from Document because it wasn't in formats -- this is extremely wasteful and indicates a bug.", ); delete document.extract; } else if (formats.has("extract") && document.extract === undefined) { meta.logger.warn( - "Request had format: extract, but there was no extract field in the result." + "Request had format: extract, but there was no extract field in the result.", ); } @@ -161,12 +161,12 @@ export const transformerStack: Transformer[] = [ uploadScreenshot, performLLMExtract, coerceFieldsToFormats, - removeBase64Images + removeBase64Images, ]; export async function executeTransformers( meta: Meta, - document: Document + document: Document, ): Promise { const executions: [string, number][] = []; @@ -174,8 +174,8 @@ export async function executeTransformers( const _meta = { ...meta, logger: meta.logger.child({ - method: "executeTransformers/" + transformer.name - }) + method: "executeTransformers/" + transformer.name, + }), }; const start = Date.now(); document = await transformer(_meta, document); diff --git a/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts b/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts index f09073ee..6380edb8 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/llmExtract.ts @@ -25,8 +25,8 @@ function normalizeSchema(x: any): any { x["$defs"] = Object.fromEntries( Object.entries(x["$defs"]).map(([name, schema]) => [ name, - normalizeSchema(schema) - ]) + normalizeSchema(schema), + ]), ); } @@ -50,15 +50,15 @@ function normalizeSchema(x: any): any { return { ...x, properties: Object.fromEntries( - Object.entries(x.properties).map(([k, v]) => [k, normalizeSchema(v)]) + Object.entries(x.properties).map(([k, v]) => [k, normalizeSchema(v)]), ), required: Object.keys(x.properties), - additionalProperties: false + additionalProperties: false, }; } else if (x && x.type === "array") { return { ...x, - items: normalizeSchema(x.items) + items: normalizeSchema(x.items), }; } else { return x; @@ -70,7 +70,7 @@ export async function generateOpenAICompletions( options: ExtractOptions, markdown?: string, previousWarning?: string, - isExtractEndpoint?: boolean + isExtractEndpoint?: boolean, ): Promise<{ extract: any; numTokens: number; warning: string | undefined }> { let extract: any; let warning: string | undefined; @@ -125,19 +125,19 @@ export async function generateOpenAICompletions( schema = { type: "object", properties: { - items: options.schema + items: options.schema, }, required: ["items"], - additionalProperties: false + additionalProperties: false, }; } else if (schema && typeof schema === "object" && !schema.type) { schema = { type: "object", properties: Object.fromEntries( - Object.entries(schema).map(([key, value]) => [key, { type: value }]) + Object.entries(schema).map(([key, value]) => [key, { type: value }]), ), required: Object.keys(schema), - additionalProperties: false + additionalProperties: false, }; } @@ -149,19 +149,19 @@ export async function generateOpenAICompletions( messages: [ { role: "system", - content: options.systemPrompt + content: options.systemPrompt, }, { role: "user", - content: [{ type: "text", text: markdown }] + content: [{ type: "text", text: markdown }], }, { role: "user", content: options.prompt !== undefined ? `Transform the above content into structured JSON output based on the following user request: ${options.prompt}` - : "Transform the above content into structured JSON output." - } + : "Transform the above content into structured JSON output.", + }, ], response_format: options.schema ? { @@ -169,10 +169,10 @@ export async function generateOpenAICompletions( json_schema: { name: "websiteContent", schema: schema, - strict: true - } + strict: true, + }, } - : { type: "json_object" } + : { type: "json_object" }, }); if (jsonCompletion.choices[0].message.refusal !== null) { @@ -187,16 +187,16 @@ export async function generateOpenAICompletions( extract = JSON.parse(jsonCompletion.choices[0].message.content); } else { const extractData = JSON.parse( - jsonCompletion.choices[0].message.content + jsonCompletion.choices[0].message.content, ); extract = options.schema ? extractData.data.extract : extractData; } } catch (e) { logger.error("Failed to parse returned JSON, no schema specified.", { - error: e + error: e, }); throw new LLMRefusalError( - "Failed to parse returned JSON. Please specify a schema in the extract object." + "Failed to parse returned JSON. Please specify a schema in the extract object.", ); } } @@ -215,16 +215,16 @@ export async function generateOpenAICompletions( export async function performLLMExtract( meta: Meta, - document: Document + document: Document, ): Promise { if (meta.options.formats.includes("extract")) { const { extract, warning } = await generateOpenAICompletions( meta.logger.child({ - method: "performLLMExtract/generateOpenAICompletions" + method: "performLLMExtract/generateOpenAICompletions", }), meta.options.extract!, document.markdown, - document.warning + document.warning, ); document.extract = extract; document.warning = warning; diff --git a/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts b/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts index 3bc408ff..aa4e937f 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/removeBase64Images.ts @@ -7,7 +7,7 @@ export function removeBase64Images(meta: Meta, document: Document): Document { if (meta.options.removeBase64Images && document.markdown !== undefined) { document.markdown = document.markdown.replace( regex, - "$1()" + "$1()", ); } return document; diff --git a/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts b/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts index ed01af69..83df17b8 100644 --- a/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts +++ b/apps/api/src/scraper/scrapeURL/transformers/uploadScreenshot.ts @@ -23,8 +23,8 @@ export function uploadScreenshot(meta: Meta, document: Document): Document { { cacheControl: "3600", upsert: false, - contentType: document.screenshot.split(":")[1].split(";")[0] - } + contentType: document.screenshot.split(":")[1].split(";")[0], + }, ); document.screenshot = `https://service.firecrawl.dev/storage/v1/object/public/media/${encodeURIComponent(fileName)}`; diff --git a/apps/api/src/search/fireEngine.ts b/apps/api/src/search/fireEngine.ts index 3fa9c588..26277523 100644 --- a/apps/api/src/search/fireEngine.ts +++ b/apps/api/src/search/fireEngine.ts @@ -15,7 +15,7 @@ export async function fireEngineMap( location?: string; numResults: number; page?: number; - } + }, ): Promise { try { let data = JSON.stringify({ @@ -25,12 +25,12 @@ export async function fireEngineMap( location: options.location, tbs: options.tbs, numResults: options.numResults, - page: options.page ?? 1 + page: options.page ?? 1, }); if (!process.env.FIRE_ENGINE_BETA_URL) { console.warn( - "(v1/map Beta) Results might differ from cloud offering currently." + "(v1/map Beta) Results might differ from cloud offering currently.", ); return []; } @@ -39,9 +39,9 @@ export async function fireEngineMap( method: "POST", headers: { "Content-Type": "application/json", - "X-Disable-Cache": "true" + "X-Disable-Cache": "true", }, - body: data + body: data, }); if (response.ok) { diff --git a/apps/api/src/search/googlesearch.ts b/apps/api/src/search/googlesearch.ts index a7c78fc9..74620651 100644 --- a/apps/api/src/search/googlesearch.ts +++ b/apps/api/src/search/googlesearch.ts @@ -11,7 +11,7 @@ const _useragent_list = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.62", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0", ]; function get_useragent(): string { @@ -27,14 +27,14 @@ async function _req( proxies: any, timeout: number, tbs: string | undefined = undefined, - filter: string | undefined = undefined + filter: string | undefined = undefined, ) { const params = { q: term, num: results, // Number of results to return hl: lang, gl: country, - start: start + start: start, }; if (tbs) { params["tbs"] = tbs; @@ -45,11 +45,11 @@ async function _req( try { const resp = await axios.get("https://www.google.com/search", { headers: { - "User-Agent": get_useragent() + "User-Agent": get_useragent(), }, params: params, proxy: proxies, - timeout: timeout + timeout: timeout, }); return resp; } catch (error) { @@ -70,7 +70,7 @@ export async function googleSearch( country = "us", proxy = undefined as string | undefined, sleep_interval = 0, - timeout = 5000 + timeout = 5000, ): Promise { let proxies: any = null; if (proxy) { @@ -98,7 +98,7 @@ export async function googleSearch( proxies, timeout, tbs, - filter + filter, ); const $ = cheerio.load(resp.data); const result_block = $("div.g"); @@ -117,7 +117,7 @@ export async function googleSearch( const title = $(element).find("h3"); const ogImage = $(element).find("img").eq(1).attr("src"); const description_box = $(element).find( - "div[style='-webkit-line-clamp:2']" + "div[style='-webkit-line-clamp:2']", ); const answerBox = $(element).find(".mod").text(); if (description_box) { @@ -129,7 +129,7 @@ export async function googleSearch( } }); await new Promise((resolve) => - setTimeout(resolve, sleep_interval * 1000) + setTimeout(resolve, sleep_interval * 1000), ); } catch (error) { if (error.message === "Too many requests") { diff --git a/apps/api/src/search/index.ts b/apps/api/src/search/index.ts index 978a57e0..82a6b68f 100644 --- a/apps/api/src/search/index.ts +++ b/apps/api/src/search/index.ts @@ -16,7 +16,7 @@ export async function search({ location = undefined, proxy = undefined, sleep_interval = 0, - timeout = 5000 + timeout = 5000, }: { query: string; advanced?: boolean; @@ -38,7 +38,7 @@ export async function search({ filter, lang, country, - location + location, }); } if (process.env.SEARCHAPI_API_KEY) { @@ -48,7 +48,7 @@ export async function search({ filter, lang, country, - location + location, }); } return await googleSearch( @@ -61,7 +61,7 @@ export async function search({ country, proxy, sleep_interval, - timeout + timeout, ); } catch (error) { logger.error(`Error in search function: ${error}`); diff --git a/apps/api/src/search/searchapi.ts b/apps/api/src/search/searchapi.ts index ea21c8d3..896c64c6 100644 --- a/apps/api/src/search/searchapi.ts +++ b/apps/api/src/search/searchapi.ts @@ -16,7 +16,7 @@ interface SearchOptions { export async function searchapi_search( q: string, - options: SearchOptions + options: SearchOptions, ): Promise { const params = { q: q, @@ -25,7 +25,7 @@ export async function searchapi_search( location: options.location, num: options.num_results, page: options.page ?? 1, - engine: process.env.SEARCHAPI_ENGINE || "google" + engine: process.env.SEARCHAPI_ENGINE || "google", }; const url = `https://www.searchapi.io/api/v1/search`; @@ -35,9 +35,9 @@ export async function searchapi_search( headers: { Authorization: `Bearer ${process.env.SEARCHAPI_API_KEY}`, "Content-Type": "application/json", - "X-SearchApi-Source": "Firecrawl" + "X-SearchApi-Source": "Firecrawl", }, - params: params + params: params, }); if (response.status === 401) { @@ -50,7 +50,7 @@ export async function searchapi_search( return data.organic_results.map((a: any) => ({ url: a.link, title: a.title, - description: a.snippet + description: a.snippet, })); } else { return []; diff --git a/apps/api/src/search/serper.ts b/apps/api/src/search/serper.ts index 4abf720d..88ff7cc0 100644 --- a/apps/api/src/search/serper.ts +++ b/apps/api/src/search/serper.ts @@ -14,7 +14,7 @@ export async function serper_search( location?: string; num_results: number; page?: number; - } + }, ): Promise { let data = JSON.stringify({ q: q, @@ -23,7 +23,7 @@ export async function serper_search( location: options.location, tbs: options.tbs, num: options.num_results, - page: options.page ?? 1 + page: options.page ?? 1, }); let config = { @@ -31,16 +31,16 @@ export async function serper_search( url: "https://google.serper.dev/search", headers: { "X-API-KEY": process.env.SERPER_API_KEY, - "Content-Type": "application/json" + "Content-Type": "application/json", }, - data: data + data: data, }; const response = await axios(config); if (response && response.data && Array.isArray(response.data.organic)) { return response.data.organic.map((a) => ({ url: a.link, title: a.title, - description: a.snippet + description: a.snippet, })); } else { return []; diff --git a/apps/api/src/services/alerts/index.ts b/apps/api/src/services/alerts/index.ts index 3aaea3aa..44f2b8a0 100644 --- a/apps/api/src/services/alerts/index.ts +++ b/apps/api/src/services/alerts/index.ts @@ -17,15 +17,15 @@ export async function checkAlerts() { const activeJobs = await scrapeQueue.getActiveCount(); if (activeJobs > Number(process.env.ALERT_NUM_ACTIVE_JOBS)) { logger.warn( - `Alert: Number of active jobs is over ${process.env.ALERT_NUM_ACTIVE_JOBS}. Current active jobs: ${activeJobs}.` + `Alert: Number of active jobs is over ${process.env.ALERT_NUM_ACTIVE_JOBS}. Current active jobs: ${activeJobs}.`, ); sendSlackWebhook( `Alert: Number of active jobs is over ${process.env.ALERT_NUM_ACTIVE_JOBS}. Current active jobs: ${activeJobs}`, - true + true, ); } else { logger.info( - `Number of active jobs is under ${process.env.ALERT_NUM_ACTIVE_JOBS}. Current active jobs: ${activeJobs}` + `Number of active jobs is under ${process.env.ALERT_NUM_ACTIVE_JOBS}. Current active jobs: ${activeJobs}`, ); } } catch (error) { @@ -39,11 +39,11 @@ export async function checkAlerts() { if (waitingJobs > Number(process.env.ALERT_NUM_WAITING_JOBS)) { logger.warn( - `Alert: Number of waiting jobs is over ${process.env.ALERT_NUM_WAITING_JOBS}. Current waiting jobs: ${waitingJobs}.` + `Alert: Number of waiting jobs is over ${process.env.ALERT_NUM_WAITING_JOBS}. Current waiting jobs: ${waitingJobs}.`, ); sendSlackWebhook( `Alert: Number of waiting jobs is over ${process.env.ALERT_NUM_WAITING_JOBS}. Current waiting jobs: ${waitingJobs}. Scale up the number of workers with fly scale count worker=20`, - true + true, ); } }; diff --git a/apps/api/src/services/alerts/slack.ts b/apps/api/src/services/alerts/slack.ts index 11280f28..ad8f9186 100644 --- a/apps/api/src/services/alerts/slack.ts +++ b/apps/api/src/services/alerts/slack.ts @@ -4,18 +4,18 @@ import { logger } from "../../../src/lib/logger"; export async function sendSlackWebhook( message: string, alertEveryone: boolean = false, - webhookUrl: string = process.env.SLACK_WEBHOOK_URL ?? "" + webhookUrl: string = process.env.SLACK_WEBHOOK_URL ?? "", ) { const messagePrefix = alertEveryone ? " " : ""; const payload = { - text: `${messagePrefix} ${message}` + text: `${messagePrefix} ${message}`, }; try { const response = await axios.post(webhookUrl, payload, { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, }); logger.info("Webhook sent successfully:", response.data); } catch (error) { diff --git a/apps/api/src/services/billing/auto_charge.ts b/apps/api/src/services/billing/auto_charge.ts index 3411c921..45fdf1f5 100644 --- a/apps/api/src/services/billing/auto_charge.ts +++ b/apps/api/src/services/billing/auto_charge.ts @@ -22,7 +22,7 @@ const AUTO_RECHARGE_COOLDOWN = 300; // 5 minutes in seconds */ export async function autoCharge( chunk: AuthCreditUsageChunk, - autoRechargeThreshold: number + autoRechargeThreshold: number, ): Promise<{ success: boolean; message: string; @@ -38,13 +38,13 @@ export async function autoCharge( const cooldownValue = await getValue(cooldownKey); if (cooldownValue) { logger.info( - `Auto-recharge for team ${chunk.team_id} is in cooldown period` + `Auto-recharge for team ${chunk.team_id} is in cooldown period`, ); return { success: false, message: "Auto-recharge is in cooldown period", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } @@ -53,7 +53,7 @@ export async function autoCharge( [resource], 5000, async ( - signal + signal, ): Promise<{ success: boolean; message: string; @@ -81,7 +81,7 @@ export async function autoCharge( success: false, message: "Error fetching customer data", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } @@ -90,7 +90,7 @@ export async function autoCharge( // Attempt to create a payment intent const paymentStatus = await createPaymentIntent( chunk.team_id, - customer.stripe_customer_id + customer.stripe_customer_id, ); // If payment is successful or requires further action, issue credits @@ -100,7 +100,7 @@ export async function autoCharge( ) { issueCreditsSuccess = await issueCredits( chunk.team_id, - AUTO_RECHARGE_CREDITS + AUTO_RECHARGE_CREDITS, ); } @@ -109,7 +109,7 @@ export async function autoCharge( team_id: chunk.team_id, initial_payment_status: paymentStatus.return_status, credits_issued: issueCreditsSuccess ? AUTO_RECHARGE_CREDITS : 0, - stripe_charge_id: paymentStatus.charge_id + stripe_charge_id: paymentStatus.charge_id, }); // Send a notification if credits were successfully issued @@ -120,7 +120,7 @@ export async function autoCharge( chunk.sub_current_period_start, chunk.sub_current_period_end, chunk, - true + true, ); // Set cooldown period @@ -139,7 +139,7 @@ export async function autoCharge( sendSlackWebhook( `Auto-recharge: Team ${chunk.team_id}. ${AUTO_RECHARGE_CREDITS} credits added. Payment status: ${paymentStatus.return_status}.`, false, - process.env.SLACK_ADMIN_WEBHOOK_URL + process.env.SLACK_ADMIN_WEBHOOK_URL, ).catch((error) => { logger.debug(`Error sending slack notification: ${error}`); }); @@ -156,8 +156,8 @@ export async function autoCharge( chunk: { ...chunk, remaining_credits: - chunk.remaining_credits + AUTO_RECHARGE_CREDITS - } + chunk.remaining_credits + AUTO_RECHARGE_CREDITS, + }, }; } else { logger.error("No Stripe customer ID found for user"); @@ -165,7 +165,7 @@ export async function autoCharge( success: false, message: "No Stripe customer ID found for user", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } } else { @@ -174,7 +174,7 @@ export async function autoCharge( success: false, message: "No sub_user_id found in chunk", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } } @@ -182,9 +182,9 @@ export async function autoCharge( success: false, message: "No need to auto-recharge", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; - } + }, ); } catch (error) { logger.error(`Failed to acquire lock for auto-recharge: ${error}`); @@ -192,7 +192,7 @@ export async function autoCharge( success: false, message: "Failed to acquire lock for auto-recharge", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } } diff --git a/apps/api/src/services/billing/credit_billing.ts b/apps/api/src/services/billing/credit_billing.ts index f25e165e..bbd04cc0 100644 --- a/apps/api/src/services/billing/credit_billing.ts +++ b/apps/api/src/services/billing/credit_billing.ts @@ -19,18 +19,18 @@ const FREE_CREDITS = 500; export async function billTeam( team_id: string, subscription_id: string | null | undefined, - credits: number + credits: number, ) { return withAuth(supaBillTeam, { success: true, message: "No DB, bypassed." })( team_id, subscription_id, - credits + credits, ); } export async function supaBillTeam( team_id: string, subscription_id: string | null | undefined, - credits: number + credits: number, ) { if (team_id === "preview") { return { success: true, message: "Preview team, no credits used" }; @@ -41,7 +41,7 @@ export async function supaBillTeam( _team_id: team_id, sub_id: subscription_id ?? null, fetch_subscription: subscription_id === undefined, - credits + credits, }); if (error) { @@ -58,9 +58,9 @@ export async function supaBillTeam( ...acuc, credits_used: acuc.credits_used + credits, adjusted_credits_used: acuc.adjusted_credits_used + credits, - remaining_credits: acuc.remaining_credits - credits + remaining_credits: acuc.remaining_credits - credits, } - : null + : null, ); } })(); @@ -76,12 +76,12 @@ export type CheckTeamCreditsResponse = { export async function checkTeamCredits( chunk: AuthCreditUsageChunk | null, team_id: string, - credits: number + credits: number, ): Promise { return withAuth(supaCheckTeamCredits, { success: true, message: "No DB, bypassed", - remainingCredits: Infinity + remainingCredits: Infinity, })(chunk, team_id, credits); } @@ -89,14 +89,14 @@ export async function checkTeamCredits( export async function supaCheckTeamCredits( chunk: AuthCreditUsageChunk | null, team_id: string, - credits: number + credits: number, ): Promise { // WARNING: chunk will be null if team_id is preview -- do not perform operations on it under ANY circumstances - mogery if (team_id === "preview") { return { success: true, message: "Preview team, no credits used", - remainingCredits: Infinity + remainingCredits: Infinity, }; } else if (chunk === null) { throw new Error("NULL ACUC passed to supaCheckTeamCredits"); @@ -141,7 +141,7 @@ export async function supaCheckTeamCredits( success: true, message: autoChargeResult.message, remainingCredits: autoChargeResult.remainingCredits, - chunk: autoChargeResult.chunk + chunk: autoChargeResult.chunk, }; } } @@ -155,7 +155,7 @@ export async function supaCheckTeamCredits( NotificationType.LIMIT_REACHED, chunk.sub_current_period_start, chunk.sub_current_period_end, - chunk + chunk, ); } return { @@ -163,7 +163,7 @@ export async function supaCheckTeamCredits( message: "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing.", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } else if (creditUsagePercentage >= 0.8 && creditUsagePercentage < 1) { // Send email notification for approaching credit limit @@ -172,7 +172,7 @@ export async function supaCheckTeamCredits( NotificationType.APPROACHING_LIMIT, chunk.sub_current_period_start, chunk.sub_current_period_end, - chunk + chunk, ); } @@ -180,13 +180,13 @@ export async function supaCheckTeamCredits( success: true, message: "Sufficient credits available", remainingCredits: chunk.remaining_credits, - chunk + chunk, }; } // Count the total credits used by a team within the current billing period and return the remaining credits. export async function countCreditsAndRemainingForCurrentBillingPeriod( - team_id: string + team_id: string, ) { // 1. Retrieve the team's active subscription based on the team_id. const { data: subscription, error: subscriptionError } = @@ -206,7 +206,7 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( if (coupons && coupons.length > 0) { couponCredits = coupons.reduce( (total, coupon) => total + coupon.credits, - 0 + 0, ); } @@ -221,20 +221,20 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( if (creditUsageError || !creditUsages) { throw new Error( - `Failed to retrieve credit usage for team_id: ${team_id}` + `Failed to retrieve credit usage for team_id: ${team_id}`, ); } const totalCreditsUsed = creditUsages.reduce( (acc, usage) => acc + usage.credits_used, - 0 + 0, ); const remainingCredits = FREE_CREDITS + couponCredits - totalCreditsUsed; return { totalCreditsUsed: totalCreditsUsed, remainingCredits, - totalCredits: FREE_CREDITS + couponCredits + totalCredits: FREE_CREDITS + couponCredits, }; } @@ -247,13 +247,13 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( if (creditUsageError || !creditUsages) { throw new Error( - `Failed to retrieve credit usage for subscription_id: ${subscription.id}` + `Failed to retrieve credit usage for subscription_id: ${subscription.id}`, ); } const totalCreditsUsed = creditUsages.reduce( (acc, usage) => acc + usage.credits_used, - 0 + 0, ); const { data: price, error: priceError } = await supabase_service @@ -264,7 +264,7 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( if (priceError || !price) { throw new Error( - `Failed to retrieve price for price_id: ${subscription.price_id}` + `Failed to retrieve price for price_id: ${subscription.price_id}`, ); } @@ -273,6 +273,6 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod( return { totalCreditsUsed, remainingCredits, - totalCredits: price.credits + totalCredits: price.credits, }; } diff --git a/apps/api/src/services/billing/issue_credits.ts b/apps/api/src/services/billing/issue_credits.ts index 3f013a1c..ce84db1b 100644 --- a/apps/api/src/services/billing/issue_credits.ts +++ b/apps/api/src/services/billing/issue_credits.ts @@ -8,7 +8,7 @@ export async function issueCredits(team_id: string, credits: number) { credits: credits, status: "active", // indicates that this coupon was issued from auto recharge - from_auto_recharge: true + from_auto_recharge: true, }); if (error) { diff --git a/apps/api/src/services/billing/stripe.ts b/apps/api/src/services/billing/stripe.ts index c5b76445..0d0b17cf 100644 --- a/apps/api/src/services/billing/stripe.ts +++ b/apps/api/src/services/billing/stripe.ts @@ -5,7 +5,7 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? ""); async function getCustomerDefaultPaymentMethod(customerId: string) { const paymentMethods = await stripe.customers.listPaymentMethods(customerId, { - limit: 3 + limit: 3, }); return paymentMethods.data[0] ?? null; } @@ -13,14 +13,14 @@ async function getCustomerDefaultPaymentMethod(customerId: string) { type ReturnStatus = "succeeded" | "requires_action" | "failed"; export async function createPaymentIntent( team_id: string, - customer_id: string + customer_id: string, ): Promise<{ return_status: ReturnStatus; charge_id: string }> { try { const defaultPaymentMethod = await getCustomerDefaultPaymentMethod(customer_id); if (!defaultPaymentMethod) { logger.error( - `No default payment method found for customer: ${customer_id}` + `No default payment method found for customer: ${customer_id}`, ); return { return_status: "failed", charge_id: "" }; } @@ -32,7 +32,7 @@ export async function createPaymentIntent( payment_method_types: [defaultPaymentMethod?.type ?? "card"], payment_method: defaultPaymentMethod?.id, off_session: true, - confirm: true + confirm: true, }); if (paymentIntent.status === "succeeded") { @@ -51,7 +51,7 @@ export async function createPaymentIntent( } } catch (error) { logger.error( - `Failed to create or confirm PaymentIntent for team: ${team_id}` + `Failed to create or confirm PaymentIntent for team: ${team_id}`, ); console.error(error); return { return_status: "failed", charge_id: "" }; diff --git a/apps/api/src/services/logging/crawl_log.ts b/apps/api/src/services/logging/crawl_log.ts index bfdc84ce..86f88529 100644 --- a/apps/api/src/services/logging/crawl_log.ts +++ b/apps/api/src/services/logging/crawl_log.ts @@ -12,8 +12,8 @@ export async function logCrawl(job_id: string, team_id: string) { .insert([ { job_id: job_id, - team_id: team_id - } + team_id: team_id, + }, ]); } catch (error) { logger.error(`Error logging crawl job to supabase:\n${error}`); diff --git a/apps/api/src/services/logging/log_job.ts b/apps/api/src/services/logging/log_job.ts index c3111dd7..b0754622 100644 --- a/apps/api/src/services/logging/log_job.ts +++ b/apps/api/src/services/logging/log_job.ts @@ -24,8 +24,8 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { job.docs = [ { content: "REDACTED DUE TO AUTHORIZATION HEADER", - html: "REDACTED DUE TO AUTHORIZATION HEADER" - } + html: "REDACTED DUE TO AUTHORIZATION HEADER", + }, ]; } const jobColumn = { @@ -43,7 +43,7 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { origin: job.origin, num_tokens: job.num_tokens, retry: !!job.retry, - crawl_id: job.crawl_id + crawl_id: job.crawl_id, }; if (force) { @@ -57,10 +57,10 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { if (error) { logger.error( "Failed to log job due to Supabase error -- trying again", - { error, scrapeId: job.job_id } + { error, scrapeId: job.job_id }, ); await new Promise((resolve) => - setTimeout(() => resolve(), 75) + setTimeout(() => resolve(), 75), ); } else { done = true; @@ -69,7 +69,7 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { } catch (error) { logger.error( "Failed to log job due to thrown error -- trying again", - { error, scrapeId: job.job_id } + { error, scrapeId: job.job_id }, ); await new Promise((resolve) => setTimeout(() => resolve(), 75)); } @@ -86,7 +86,7 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { if (error) { logger.error(`Error logging job: ${error.message}`, { error, - scrapeId: job.job_id + scrapeId: job.job_id, }); } else { logger.debug("Job logged successfully!", { scrapeId: job.job_id }); @@ -97,7 +97,7 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { let phLog = { distinctId: "from-api", //* To identify this on the group level, setting distinctid to a static string per posthog docs: https://posthog.com/docs/product-analytics/group-analytics#advanced-server-side-only-capturing-group-events-without-a-user ...(job.team_id !== "preview" && { - groups: { team: job.team_id } + groups: { team: job.team_id }, }), //* Identifying event on this team event: "job-logged", properties: { @@ -112,8 +112,8 @@ export async function logJob(job: FirecrawlJob, force: boolean = false) { page_options: job.scrapeOptions, origin: job.origin, num_tokens: job.num_tokens, - retry: job.retry - } + retry: job.retry, + }, }; if (job.mode !== "single_urls") { posthog.capture(phLog); diff --git a/apps/api/src/services/logging/scrape_log.ts b/apps/api/src/services/logging/scrape_log.ts index 3ccaf777..6e076330 100644 --- a/apps/api/src/services/logging/scrape_log.ts +++ b/apps/api/src/services/logging/scrape_log.ts @@ -8,7 +8,7 @@ configDotenv(); export async function logScrape( scrapeLog: ScrapeLog, - pageOptions?: PageOptions + pageOptions?: PageOptions, ) { const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; if (!useDbAuthentication) { @@ -42,8 +42,8 @@ export async function logScrape( date_added: new Date().toISOString(), html: "Removed to save db space", ipv4_support: scrapeLog.ipv4_support, - ipv6_support: scrapeLog.ipv6_support - } + ipv6_support: scrapeLog.ipv6_support, + }, ]); if (error) { diff --git a/apps/api/src/services/notification/email_notification.ts b/apps/api/src/services/notification/email_notification.ts index 22c23865..6f310e5e 100644 --- a/apps/api/src/services/notification/email_notification.ts +++ b/apps/api/src/services/notification/email_notification.ts @@ -14,25 +14,25 @@ const emailTemplates: Record< > = { [NotificationType.APPROACHING_LIMIT]: { subject: "You've used 80% of your credit limit - Firecrawl", - html: "Hey there,

You are approaching your credit limit for this billing period. Your usage right now is around 80% of your total credit limit. Consider upgrading your plan to avoid hitting the limit. Check out our pricing page for more info.


Thanks,
Firecrawl Team
" + html: "Hey there,

You are approaching your credit limit for this billing period. Your usage right now is around 80% of your total credit limit. Consider upgrading your plan to avoid hitting the limit. Check out our pricing page for more info.


Thanks,
Firecrawl Team
", }, [NotificationType.LIMIT_REACHED]: { subject: "Credit Limit Reached! Take action now to resume usage - Firecrawl", - html: "Hey there,

You have reached your credit limit for this billing period. To resume usage, please upgrade your plan. Check out our pricing page for more info.


Thanks,
Firecrawl Team
" + html: "Hey there,

You have reached your credit limit for this billing period. To resume usage, please upgrade your plan. Check out our pricing page for more info.


Thanks,
Firecrawl Team
", }, [NotificationType.RATE_LIMIT_REACHED]: { subject: "Rate Limit Reached - Firecrawl", - html: "Hey there,

You've hit one of the Firecrawl endpoint's rate limit! Take a breather and try again in a few moments. If you need higher rate limits, consider upgrading your plan. Check out our pricing page for more info.

If you have any questions, feel free to reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team

Ps. this email is only sent once every 7 days if you reach a rate limit." + html: "Hey there,

You've hit one of the Firecrawl endpoint's rate limit! Take a breather and try again in a few moments. If you need higher rate limits, consider upgrading your plan. Check out our pricing page for more info.

If you have any questions, feel free to reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team

Ps. this email is only sent once every 7 days if you reach a rate limit.", }, [NotificationType.AUTO_RECHARGE_SUCCESS]: { subject: "Auto recharge successful - Firecrawl", - html: "Hey there,

Your account was successfully recharged with 1000 credits because your remaining credits were below the threshold. Consider upgrading your plan at firecrawl.dev/pricing to avoid hitting the limit.


Thanks,
Firecrawl Team
" + html: "Hey there,

Your account was successfully recharged with 1000 credits because your remaining credits were below the threshold. Consider upgrading your plan at firecrawl.dev/pricing to avoid hitting the limit.


Thanks,
Firecrawl Team
", }, [NotificationType.AUTO_RECHARGE_FAILED]: { subject: "Auto recharge failed - Firecrawl", - html: "Hey there,

Your auto recharge failed. Please try again manually. If the issue persists, please reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team
" - } + html: "Hey there,

Your auto recharge failed. Please try again manually. If the issue persists, please reach out to us at help@firecrawl.com


Thanks,
Firecrawl Team
", + }, }; export async function sendNotification( @@ -41,7 +41,7 @@ export async function sendNotification( startDateString: string | null, endDateString: string | null, chunk: AuthCreditUsageChunk, - bypassRecentChecks: boolean = false + bypassRecentChecks: boolean = false, ) { return withAuth(sendNotificationInternal, undefined)( team_id, @@ -49,13 +49,13 @@ export async function sendNotification( startDateString, endDateString, chunk, - bypassRecentChecks + bypassRecentChecks, ); } export async function sendEmailNotification( email: string, - notificationType: NotificationType + notificationType: NotificationType, ) { const resend = new Resend(process.env.RESEND_API_KEY); @@ -65,7 +65,7 @@ export async function sendEmailNotification( to: [email], reply_to: "help@firecrawl.com", subject: emailTemplates[notificationType].subject, - html: emailTemplates[notificationType].html + html: emailTemplates[notificationType].html, }); if (error) { @@ -84,7 +84,7 @@ export async function sendNotificationInternal( startDateString: string | null, endDateString: string | null, chunk: AuthCreditUsageChunk, - bypassRecentChecks: boolean = false + bypassRecentChecks: boolean = false, ): Promise<{ success: boolean }> { if (team_id === "preview") { return { success: true }; @@ -125,7 +125,7 @@ export async function sendNotificationInternal( if (recentError) { logger.debug( - `Error fetching recent notifications: ${recentError.message}` + `Error fetching recent notifications: ${recentError.message}`, ); return { success: false }; } @@ -136,7 +136,7 @@ export async function sendNotificationInternal( } console.log( - `Sending notification for team_id: ${team_id} and notificationType: ${notificationType}` + `Sending notification for team_id: ${team_id} and notificationType: ${notificationType}`, ); // get the emails from the user with the team_id const { data: emails, error: emailsError } = await supabase_service @@ -160,15 +160,15 @@ export async function sendNotificationInternal( team_id: team_id, notification_type: notificationType, sent_date: new Date().toISOString(), - timestamp: new Date().toISOString() - } + timestamp: new Date().toISOString(), + }, ]); if (process.env.SLACK_ADMIN_WEBHOOK_URL && emails.length > 0) { sendSlackWebhook( `${getNotificationString(notificationType)}: Team ${team_id}, with email ${emails[0].email}. Number of credits used: ${chunk.adjusted_credits_used} | Number of credits in the plan: ${chunk.price_credits}`, false, - process.env.SLACK_ADMIN_WEBHOOK_URL + process.env.SLACK_ADMIN_WEBHOOK_URL, ).catch((error) => { logger.debug(`Error sending slack notification: ${error}`); }); @@ -180,6 +180,6 @@ export async function sendNotificationInternal( } return { success: true }; - } + }, ); } diff --git a/apps/api/src/services/notification/notification_string.ts b/apps/api/src/services/notification/notification_string.ts index 72bc60c4..46da76e0 100644 --- a/apps/api/src/services/notification/notification_string.ts +++ b/apps/api/src/services/notification/notification_string.ts @@ -2,7 +2,7 @@ import { NotificationType } from "../../types"; // depending on the notification type, return the appropriate string export function getNotificationString( - notificationType: NotificationType + notificationType: NotificationType, ): string { switch (notificationType) { case NotificationType.APPROACHING_LIMIT: diff --git a/apps/api/src/services/posthog.ts b/apps/api/src/services/posthog.ts index 69f370ec..3f56123c 100644 --- a/apps/api/src/services/posthog.ts +++ b/apps/api/src/services/posthog.ts @@ -6,7 +6,7 @@ export default function PostHogClient(apiKey: string) { const posthogClient = new PostHog(apiKey, { host: process.env.POSTHOG_HOST, flushAt: 1, - flushInterval: 0 + flushInterval: 0, }); return posthogClient; } @@ -21,7 +21,7 @@ export const posthog = process.env.POSTHOG_API_KEY ? PostHogClient(process.env.POSTHOG_API_KEY) : (() => { logger.warn( - "POSTHOG_API_KEY is not provided - your events will not be logged. Using MockPostHog as a fallback. See posthog.ts for more." + "POSTHOG_API_KEY is not provided - your events will not be logged. Using MockPostHog as a fallback. See posthog.ts for more.", ); return new MockPostHog(); })(); diff --git a/apps/api/src/services/queue-jobs.ts b/apps/api/src/services/queue-jobs.ts index b4bd799b..bd2b9121 100644 --- a/apps/api/src/services/queue-jobs.ts +++ b/apps/api/src/services/queue-jobs.ts @@ -8,14 +8,14 @@ import { getConcurrencyLimitActiveJobs, getConcurrencyLimitMax, pushConcurrencyLimitActiveJob, - pushConcurrencyLimitedJob + pushConcurrencyLimitedJob, } from "../lib/concurrency-limit"; async function addScrapeJobRaw( webScraperOptions: any, options: any, jobId: string, - jobPriority: number = 10 + jobPriority: number = 10, ) { let concurrencyLimited = false; @@ -39,9 +39,9 @@ async function addScrapeJobRaw( opts: { ...options, priority: jobPriority, - jobId: jobId + jobId: jobId, }, - priority: jobPriority + priority: jobPriority, }); } else { if ( @@ -55,7 +55,7 @@ async function addScrapeJobRaw( await getScrapeQueue().add(jobId, webScraperOptions, { ...options, priority: jobPriority, - jobId + jobId, }); } } @@ -64,7 +64,7 @@ export async function addScrapeJob( webScraperOptions: WebScraperOptions, options: any = {}, jobId: string = uuidv4(), - jobPriority: number = 10 + jobPriority: number = 10, ) { if (Sentry.isInitialized()) { const size = JSON.stringify(webScraperOptions).length; @@ -75,8 +75,8 @@ export async function addScrapeJob( attributes: { "messaging.message.id": jobId, "messaging.destination.name": getScrapeQueue().name, - "messaging.message.body.size": size - } + "messaging.message.body.size": size, + }, }, async (span) => { await addScrapeJobRaw( @@ -85,14 +85,14 @@ export async function addScrapeJob( sentry: { trace: Sentry.spanToTraceHeader(span), baggage: Sentry.spanToBaggageHeader(span), - size - } + size, + }, }, options, jobId, - jobPriority + jobPriority, ); - } + }, ); } else { await addScrapeJobRaw(webScraperOptions, options, jobId, jobPriority); @@ -106,19 +106,19 @@ export async function addScrapeJobs( jobId: string; priority: number; }; - }[] + }[], ) { // TODO: better await Promise.all( jobs.map((job) => - addScrapeJob(job.data, job.opts, job.opts.jobId, job.opts.priority) - ) + addScrapeJob(job.data, job.opts, job.opts.jobId, job.opts.priority), + ), ); } export function waitForJob( jobId: string, - timeout: number + timeout: number, ): Promise { return new Promise((resolve, reject) => { const start = Date.now(); diff --git a/apps/api/src/services/queue-service.ts b/apps/api/src/services/queue-service.ts index 3970a6e7..3cfd8c91 100644 --- a/apps/api/src/services/queue-service.ts +++ b/apps/api/src/services/queue-service.ts @@ -5,7 +5,7 @@ import IORedis from "ioredis"; let scrapeQueue: Queue; export const redisConnection = new IORedis(process.env.REDIS_URL!, { - maxRetriesPerRequest: null + maxRetriesPerRequest: null, }); export const scrapeQueueName = "{scrapeQueue}"; @@ -18,13 +18,13 @@ export function getScrapeQueue() { connection: redisConnection, defaultJobOptions: { removeOnComplete: { - age: 90000 // 25 hours + age: 90000, // 25 hours }, removeOnFail: { - age: 90000 // 25 hours - } - } - } + age: 90000, // 25 hours + }, + }, + }, // { // settings: { // lockDuration: 1 * 60 * 1000, // 1 minute in milliseconds, diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index dc352d36..29f4b84f 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -5,7 +5,7 @@ import { CustomError } from "../lib/custom-error"; import { getScrapeQueue, redisConnection, - scrapeQueueName + scrapeQueueName, } from "./queue-service"; import { startWebScraperPipeline } from "../main/runWebScraper"; import { callWebhook } from "./webhook"; @@ -24,14 +24,14 @@ import { getCrawl, getCrawlJobs, lockURL, - normalizeURL + normalizeURL, } from "../lib/crawl-redis"; import { StoredCrawl } from "../lib/crawl-redis"; import { addScrapeJob } from "./queue-jobs"; import { addJobPriority, deleteJobPriority, - getJobPriority + getJobPriority, } from "../../src/lib/job-priority"; import { PlanType, RateLimiterMode } from "../types"; import { getJobs } from "..//controllers/v1/crawl-status"; @@ -42,7 +42,7 @@ import { cleanOldConcurrencyLimitEntries, pushConcurrencyLimitActiveJob, removeConcurrencyLimitActiveJob, - takeConcurrencyLimitedJob + takeConcurrencyLimitedJob, } from "../lib/concurrency-limit"; configDotenv(); @@ -74,7 +74,7 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { const jobIDs = await getCrawlJobs(job.data.crawl_id); const jobs = (await getJobs(jobIDs)).sort( - (a, b) => a.timestamp - b.timestamp + (a, b) => a.timestamp - b.timestamp, ); // const jobStatuses = await Promise.all(jobs.map((x) => x.getState())); const jobStatus = sc.cancelled // || jobStatuses.some((x) => x === "failed") @@ -87,7 +87,7 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { ? Array.isArray(x.returnvalue) ? x.returnvalue[0] : x.returnvalue - : null + : null, ) .filter((x) => x !== null); @@ -103,7 +103,7 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { url: sc.originUrl!, scrapeOptions: sc.scrapeOptions, crawlerOptions: sc.crawlerOptions, - origin: job.data.origin + origin: job.data.origin, }); const data = { @@ -112,12 +112,12 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { links: fullDocs.map((doc) => { return { content: doc, - source: doc?.metadata?.sourceURL ?? doc?.url ?? "" + source: doc?.metadata?.sourceURL ?? doc?.url ?? "", }; - }) + }), }, project_id: job.data.project_id, - docs: fullDocs + docs: fullDocs, }; // v0 web hooks, call when done with all the data @@ -130,7 +130,7 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { job.data.v1, job.data.crawlerOptions !== null ? "crawl.completed" - : "batch_scrape.completed" + : "batch_scrape.completed", ); } } else { @@ -147,7 +147,7 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { job.data.v1, job.data.crawlerOptions !== null ? "crawl.completed" - : "batch_scrape.completed" + : "batch_scrape.completed", ); } @@ -166,9 +166,9 @@ async function finishCrawlIfNeeded(job: Job & { id: string }, sc: StoredCrawl) { sc?.originUrl ?? (job.data.crawlerOptions === null ? "Batch Scrape" : "Unknown"), crawlerOptions: sc.crawlerOptions, - origin: job.data.origin + origin: job.data.origin, }, - true + true, ); } } @@ -180,7 +180,7 @@ const processJobInternal = async (token: string, job: Job & { id: string }) => { method: "processJobInternal", jobId: job.id, scrapeId: job.id, - crawlId: job.data?.crawl_id ?? undefined + crawlId: job.data?.crawl_id ?? undefined, }); const extendLockInterval = setInterval(async () => { @@ -196,7 +196,7 @@ const processJobInternal = async (token: string, job: Job & { id: string }) => { try { if (job.data.crawl_id && process.env.USE_DB_AUTHENTICATION === "true") { logger.debug( - "Job succeeded -- has crawl associated, putting null in Redis" + "Job succeeded -- has crawl associated, putting null in Redis", ); await job.moveToCompleted(null, token, false); } else { @@ -237,7 +237,7 @@ let cantAcceptConnectionCount = 0; const workerFun = async ( queue: Queue, - processJobInternal: (token: string, job: Job) => Promise + processJobInternal: (token: string, job: Job) => Promise, ) => { const logger = _logger.child({ module: "queue-worker", method: "workerFun" }); @@ -246,7 +246,7 @@ const workerFun = async ( lockDuration: 1 * 60 * 1000, // 1 minute // lockRenewTime: 15 * 1000, // 15 seconds stalledInterval: 30 * 1000, // 30 seconds - maxStalledCount: 10 // 10 times + maxStalledCount: 10, // 10 times }); worker.startStalledCheckTimer(); @@ -267,7 +267,7 @@ const workerFun = async ( if (cantAcceptConnectionCount >= 25) { logger.error("WORKER STALLED", { cpuUsage: await monitor.checkCpuUsage(), - memoryUsage: await monitor.checkMemoryUsage() + memoryUsage: await monitor.checkMemoryUsage(), }); } @@ -295,13 +295,13 @@ const workerFun = async ( nextJob.id, { ...nextJob.data, - concurrencyLimitHit: true + concurrencyLimitHit: true, }, { ...nextJob.opts, jobId: nextJob.id, - priority: nextJob.priority - } + priority: nextJob.priority, + }, ); } } @@ -311,7 +311,7 @@ const workerFun = async ( Sentry.continueTrace( { sentryTrace: job.data.sentry.trace, - baggage: job.data.sentry.baggage + baggage: job.data.sentry.baggage, }, () => { Sentry.startSpan( @@ -319,8 +319,8 @@ const workerFun = async ( name: "Scrape job", attributes: { job: job.id, - worker: process.env.FLY_MACHINE_ID ?? worker.id - } + worker: process.env.FLY_MACHINE_ID ?? worker.id, + }, }, async (span) => { await Sentry.startSpan( @@ -333,8 +333,8 @@ const workerFun = async ( "messaging.message.body.size": job.data.sentry.size, "messaging.message.receive.latency": Date.now() - (job.processedOn ?? job.timestamp), - "messaging.message.retry.count": job.attemptsMade - } + "messaging.message.retry.count": job.attemptsMade, + }, }, async () => { let res; @@ -349,11 +349,11 @@ const workerFun = async ( } else { span.setStatus({ code: 1 }); // OK } - } + }, ); - } + }, ); - } + }, ); } else { Sentry.startSpan( @@ -361,12 +361,12 @@ const workerFun = async ( name: "Scrape job", attributes: { job: job.id, - worker: process.env.FLY_MACHINE_ID ?? worker.id - } + worker: process.env.FLY_MACHINE_ID ?? worker.id, + }, }, () => { processJobInternal(token, job).finally(() => afterJobDone(job)); - } + }, ); } @@ -385,7 +385,7 @@ async function processJob(job: Job & { id: string }, token: string) { method: "processJob", jobId: job.id, scrapeId: job.id, - crawlId: job.data?.crawl_id ?? undefined + crawlId: job.data?.crawl_id ?? undefined, }); logger.info(`🐂 Worker taking job ${job.id}`, { url: job.data.url }); @@ -403,7 +403,7 @@ async function processJob(job: Job & { id: string }, token: string) { document: null, project_id: job.data.project_id, error: - "URL is blocked. Suspecious activity detected. Please contact help@firecrawl.com if you believe this is an error." + "URL is blocked. Suspecious activity detected. Please contact help@firecrawl.com if you believe this is an error.", }; return data; } @@ -413,23 +413,23 @@ async function processJob(job: Job & { id: string }, token: string) { current: 1, total: 100, current_step: "SCRAPING", - current_url: "" + current_url: "", }); const start = Date.now(); const pipeline = await Promise.race([ startWebScraperPipeline({ job, - token + token, }), ...(job.data.scrapeOptions.timeout !== undefined ? [ (async () => { await sleep(job.data.scrapeOptions.timeout); throw new Error("timeout"); - })() + })(), ] - : []) + : []), ]); if (!pipeline.success) { @@ -450,17 +450,17 @@ async function processJob(job: Job & { id: string }, token: string) { links: [ { content: doc, - source: doc?.metadata?.sourceURL ?? doc?.metadata?.url ?? "" - } - ] + source: doc?.metadata?.sourceURL ?? doc?.metadata?.url ?? "", + }, + ], }, project_id: job.data.project_id, - document: doc + document: doc, }; if (job.data.webhook && job.data.mode !== "crawl" && job.data.v1) { logger.debug("Calling webhook with success...", { - webhook: job.data.webhook + webhook: job.data.webhook, }); await callWebhook( job.data.team_id, @@ -469,7 +469,7 @@ async function processJob(job: Job & { id: string }, token: string) { job.data.webhook, job.data.v1, job.data.crawlerOptions !== null ? "crawl.page" : "batch_scrape.page", - true + true, ); } @@ -484,18 +484,18 @@ async function processJob(job: Job & { id: string }, token: string) { ) { logger.debug( "Was redirected, removing old URL and locking new URL...", - { oldUrl: doc.metadata.sourceURL, newUrl: doc.metadata.url } + { oldUrl: doc.metadata.sourceURL, newUrl: doc.metadata.url }, ); // Remove the old URL from visited unique due to checking for limit // Do not remove from :visited otherwise it will keep crawling the original URL (sourceURL) await redisConnection.srem( "crawl:" + job.data.crawl_id + ":visited_unique", - normalizeURL(doc.metadata.sourceURL, sc) + normalizeURL(doc.metadata.sourceURL, sc), ); const p1 = generateURLPermutations(normalizeURL(doc.metadata.url, sc)); const p2 = generateURLPermutations( - normalizeURL(doc.metadata.sourceURL, sc) + normalizeURL(doc.metadata.sourceURL, sc), ); // In crawls, we should only crawl a redirected page once, no matter how many; times it is redirected to, or if it's been discovered by the crawler before. @@ -525,9 +525,9 @@ async function processJob(job: Job & { id: string }, token: string) { crawlerOptions: sc.crawlerOptions, scrapeOptions: job.data.scrapeOptions, origin: job.data.origin, - crawl_id: job.data.crawl_id + crawl_id: job.data.crawl_id, }, - true + true, ); logger.debug("Declaring job as done..."); @@ -538,19 +538,19 @@ async function processJob(job: Job & { id: string }, token: string) { const crawler = crawlToCrawler( job.data.crawl_id, sc, - doc.metadata.url ?? doc.metadata.sourceURL ?? sc.originUrl! + doc.metadata.url ?? doc.metadata.sourceURL ?? sc.originUrl!, ); const links = crawler.filterLinks( crawler.extractLinksFromHTML( rawHtml ?? "", - doc.metadata?.url ?? doc.metadata?.sourceURL ?? sc.originUrl! + doc.metadata?.url ?? doc.metadata?.sourceURL ?? sc.originUrl!, ), Infinity, - sc.crawlerOptions?.maxDepth ?? 10 + sc.crawlerOptions?.maxDepth ?? 10, ); logger.debug("Discovered " + links.length + " links...", { - linksLength: links.length + linksLength: links.length, }); for (const link of links) { @@ -559,7 +559,7 @@ async function processJob(job: Job & { id: string }, token: string) { const jobPriority = await getJobPriority({ plan: sc.plan as PlanType, team_id: sc.team_id, - basePriority: job.data.crawl_id ? 20 : 10 + basePriority: job.data.crawl_id ? 20 : 10, }); const jobId = uuidv4(); @@ -568,7 +568,7 @@ async function processJob(job: Job & { id: string }, token: string) { jobPriority + " for URL " + JSON.stringify(link), - { jobPriority, url: link } + { jobPriority, url: link }, ); // console.log("plan: ", sc.plan); @@ -587,22 +587,22 @@ async function processJob(job: Job & { id: string }, token: string) { origin: job.data.origin, crawl_id: job.data.crawl_id, webhook: job.data.webhook, - v1: job.data.v1 + v1: job.data.v1, }, {}, jobId, - jobPriority + jobPriority, ); await addCrawlJob(job.data.crawl_id, jobId); logger.debug("Added job for URL " + JSON.stringify(link), { jobPriority, url: link, - newJobId: jobId + newJobId: jobId, }); } else { logger.debug("Could not lock URL " + JSON.stringify(link), { - url: link + url: link, }); } } @@ -627,8 +627,8 @@ async function processJob(job: Job & { id: string }, token: string) { Sentry.captureException(error, { data: { - job: job.id - } + job: job.id, + }, }); if (error instanceof CustomError) { @@ -650,7 +650,7 @@ async function processJob(job: Job & { id: string }, token: string) { ? error : typeof error === "string" ? new Error(error) - : new Error(JSON.stringify(error)) + : new Error(JSON.stringify(error)), }; if (!job.data.v1 && (job.data.mode === "crawl" || job.data.crawl_id)) { @@ -660,7 +660,7 @@ async function processJob(job: Job & { id: string }, token: string) { data, job.data.webhook, job.data.v1, - job.data.crawlerOptions !== null ? "crawl.page" : "batch_scrape.page" + job.data.crawlerOptions !== null ? "crawl.page" : "batch_scrape.page", ); } // if (job.data.v1) { @@ -699,9 +699,9 @@ async function processJob(job: Job & { id: string }, token: string) { crawlerOptions: sc.crawlerOptions, scrapeOptions: job.data.scrapeOptions, origin: job.data.origin, - crawl_id: job.data.crawl_id + crawl_id: job.data.crawl_id, }, - true + true, ); await finishCrawlIfNeeded(job, sc); diff --git a/apps/api/src/services/rate-limiter.test.ts b/apps/api/src/services/rate-limiter.test.ts index 5c25a8d7..098a657c 100644 --- a/apps/api/src/services/rate-limiter.test.ts +++ b/apps/api/src/services/rate-limiter.test.ts @@ -2,7 +2,7 @@ import { getRateLimiter, serverRateLimiter, testSuiteRateLimiter, - redisRateLimitClient + redisRateLimitClient, } from "./rate-limiter"; import { RateLimiterMode } from "../../src/types"; import { RateLimiterRedis } from "rate-limiter-flexible"; @@ -33,13 +33,13 @@ describe("Rate Limiter Service", () => { it("should return the testSuiteRateLimiter for specific tokens", () => { const limiter = getRateLimiter( "crawl" as RateLimiterMode, - "test-prefix:a01ccae" + "test-prefix:a01ccae", ); expect(limiter).toBe(testSuiteRateLimiter); const limiter2 = getRateLimiter( "scrape" as RateLimiterMode, - "test-prefix:6254cf9" + "test-prefix:6254cf9", ); expect(limiter2).toBe(testSuiteRateLimiter); }); @@ -47,7 +47,7 @@ describe("Rate Limiter Service", () => { it("should return the serverRateLimiter if mode is not found", () => { const limiter = getRateLimiter( "nonexistent" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter.points).toBe(serverRateLimiter.points); }); @@ -56,28 +56,28 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "crawl" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(2); const limiter2 = getRateLimiter( "scrape" as RateLimiterMode, "test-prefix:someToken", - "standard" + "standard", ); expect(limiter2.points).toBe(100); const limiter3 = getRateLimiter( "search" as RateLimiterMode, "test-prefix:someToken", - "growth" + "growth", ); expect(limiter3.points).toBe(500); const limiter4 = getRateLimiter( "crawlStatus" as RateLimiterMode, "test-prefix:someToken", - "growth" + "growth", ); expect(limiter4.points).toBe(250); }); @@ -85,13 +85,13 @@ describe("Rate Limiter Service", () => { it("should return the default rate limiter if plan is not provided", () => { const limiter = getRateLimiter( "crawl" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter.points).toBe(3); const limiter2 = getRateLimiter( "scrape" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(20); }); @@ -103,7 +103,7 @@ describe("Rate Limiter Service", () => { storeClient: redisRateLimitClient, keyPrefix, points, - duration: 60 + duration: 60, }); expect(limiter.keyPrefix).toBe(keyPrefix); @@ -115,13 +115,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "preview" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(5); const limiter2 = getRateLimiter( "preview" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(5); }); @@ -130,13 +130,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "account" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(100); const limiter2 = getRateLimiter( "account" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(100); }); @@ -145,13 +145,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "crawlStatus" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(150); const limiter2 = getRateLimiter( "crawlStatus" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(250); }); @@ -160,13 +160,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "crawl" as RateLimiterMode, "test-prefix:someTokenCRAWL", - "free" + "free", ); const consumePoints = 1; const res = await limiter.consume( "test-prefix:someTokenCRAWL", - consumePoints + consumePoints, ); expect(res.remainingPoints).toBe(1); }); @@ -174,7 +174,7 @@ describe("Rate Limiter Service", () => { it("should consume points correctly for 'scrape' mode (DEFAULT)", async () => { const limiter = getRateLimiter( "scrape" as RateLimiterMode, - "test-prefix:someTokenX" + "test-prefix:someTokenX", ); const consumePoints = 4; @@ -186,7 +186,7 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "scrape" as RateLimiterMode, "test-prefix:someTokenXY", - "hobby" + "hobby", ); expect(limiter.points).toBe(20); @@ -201,21 +201,21 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "crawl" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(2); const limiter2 = getRateLimiter( "crawl" as RateLimiterMode, "test-prefix:someToken", - "starter" + "starter", ); expect(limiter2.points).toBe(10); const limiter3 = getRateLimiter( "crawl" as RateLimiterMode, "test-prefix:someToken", - "standard" + "standard", ); expect(limiter3.points).toBe(5); }); @@ -224,28 +224,28 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "scrape" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(10); const limiter2 = getRateLimiter( "scrape" as RateLimiterMode, "test-prefix:someToken", - "starter" + "starter", ); expect(limiter2.points).toBe(100); const limiter3 = getRateLimiter( "scrape" as RateLimiterMode, "test-prefix:someToken", - "standard" + "standard", ); expect(limiter3.points).toBe(100); const limiter4 = getRateLimiter( "scrape" as RateLimiterMode, "test-prefix:someToken", - "growth" + "growth", ); expect(limiter4.points).toBe(1000); }); @@ -254,21 +254,21 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "search" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(5); const limiter2 = getRateLimiter( "search" as RateLimiterMode, "test-prefix:someToken", - "starter" + "starter", ); expect(limiter2.points).toBe(50); const limiter3 = getRateLimiter( "search" as RateLimiterMode, "test-prefix:someToken", - "standard" + "standard", ); expect(limiter3.points).toBe(50); }); @@ -277,13 +277,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "preview" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(5); const limiter2 = getRateLimiter( "preview" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(5); }); @@ -292,13 +292,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "account" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(100); const limiter2 = getRateLimiter( "account" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(100); }); @@ -307,13 +307,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "crawlStatus" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(150); const limiter2 = getRateLimiter( "crawlStatus" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(250); }); @@ -322,13 +322,13 @@ describe("Rate Limiter Service", () => { const limiter = getRateLimiter( "testSuite" as RateLimiterMode, "test-prefix:someToken", - "free" + "free", ); expect(limiter.points).toBe(10000); const limiter2 = getRateLimiter( "testSuite" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); expect(limiter2.points).toBe(10000); }); @@ -336,7 +336,7 @@ describe("Rate Limiter Service", () => { it("should throw an error when consuming more points than available", async () => { const limiter = getRateLimiter( "crawl" as RateLimiterMode, - "test-prefix:someToken" + "test-prefix:someToken", ); const consumePoints = limiter.points + 1; @@ -357,7 +357,7 @@ describe("Rate Limiter Service", () => { storeClient: redisRateLimitClient, keyPrefix, points, - duration + duration, }); const consumePoints = 5; diff --git a/apps/api/src/services/rate-limiter.ts b/apps/api/src/services/rate-limiter.ts index 8067f862..5b8e39ca 100644 --- a/apps/api/src/services/rate-limiter.ts +++ b/apps/api/src/services/rate-limiter.ts @@ -18,7 +18,7 @@ const RATE_LIMITS = { etier2c: 300, etier1a: 1000, etier2a: 300, - etierscale1: 150 + etierscale1: 150, }, scrape: { default: 20, @@ -35,7 +35,7 @@ const RATE_LIMITS = { etier2c: 2500, etier1a: 1000, etier2a: 2500, - etierscale1: 1500 + etierscale1: 1500, }, search: { default: 20, @@ -52,7 +52,7 @@ const RATE_LIMITS = { etier2c: 2500, etier1a: 1000, etier2a: 2500, - etierscale1: 1500 + etierscale1: 1500, }, map: { default: 20, @@ -69,28 +69,28 @@ const RATE_LIMITS = { etier2c: 2500, etier1a: 1000, etier2a: 2500, - etierscale1: 1500 + etierscale1: 1500, }, preview: { free: 5, - default: 5 + default: 5, }, account: { free: 100, - default: 100 + default: 100, }, crawlStatus: { free: 300, - default: 500 + default: 500, }, testSuite: { free: 10000, - default: 10000 - } + default: 10000, + }, }; export const redisRateLimitClient = new Redis( - process.env.REDIS_RATE_LIMIT_URL! + process.env.REDIS_RATE_LIMIT_URL!, ); const createRateLimiter = (keyPrefix, points) => @@ -98,54 +98,54 @@ const createRateLimiter = (keyPrefix, points) => storeClient: redisRateLimitClient, keyPrefix, points, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); export const serverRateLimiter = createRateLimiter( "server", - RATE_LIMITS.account.default + RATE_LIMITS.account.default, ); export const testSuiteRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "test-suite", points: 10000, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); export const devBRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "dev-b", points: 1200, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); export const manualRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "manual", points: 2000, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); export const scrapeStatusRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "scrape-status", points: 400, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); export const etier1aRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "etier1a", points: 10000, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); export const etier2aRateLimiter = new RateLimiterRedis({ storeClient: redisRateLimitClient, keyPrefix: "etier2a", points: 2500, - duration: 60 // Duration in seconds + duration: 60, // Duration in seconds }); const testSuiteTokens = [ @@ -165,7 +165,7 @@ const testSuiteTokens = [ "fd769b2", "4c2638d", "cbb3462", // don't remove (s-ai) - "824abcd" // don't remove (s-ai) + "824abcd", // don't remove (s-ai) ]; const manual = ["69be9e74-7624-4990-b20d-08e0acc70cf6"]; @@ -178,7 +178,7 @@ export function getRateLimiterPoints( mode: RateLimiterMode, token?: string, plan?: string, - teamId?: string + teamId?: string, ): number { const rateLimitConfig = RATE_LIMITS[mode]; // {default : 5} @@ -193,7 +193,7 @@ export function getRateLimiter( mode: RateLimiterMode, token?: string, plan?: string, - teamId?: string + teamId?: string, ): RateLimiterRedis { if (token && testSuiteTokens.some((testToken) => token.includes(testToken))) { return testSuiteRateLimiter; @@ -221,6 +221,6 @@ export function getRateLimiter( return createRateLimiter( `${mode}-${makePlanKey(plan)}`, - getRateLimiterPoints(mode, token, plan, teamId) + getRateLimiterPoints(mode, token, plan, teamId), ); } diff --git a/apps/api/src/services/redis.ts b/apps/api/src/services/redis.ts index 04fcbd5e..d2c7dd3a 100644 --- a/apps/api/src/services/redis.ts +++ b/apps/api/src/services/redis.ts @@ -39,7 +39,7 @@ const setValue = async ( key: string, value: string, expire?: number, - nx = false + nx = false, ) => { if (expire && !nx) { await redisRateLimitClient.set(key, value, "EX", expire); diff --git a/apps/api/src/services/redlock.ts b/apps/api/src/services/redlock.ts index 757346f9..923cfc3d 100644 --- a/apps/api/src/services/redlock.ts +++ b/apps/api/src/services/redlock.ts @@ -21,6 +21,6 @@ export const redlock = new Redlock( // The minimum remaining time on a lock before an extension is automatically // attempted with the `using` API. - automaticExtensionThreshold: 500 // time in ms - } + automaticExtensionThreshold: 500, // time in ms + }, ); diff --git a/apps/api/src/services/sentry.ts b/apps/api/src/services/sentry.ts index 41f19362..927b33c3 100644 --- a/apps/api/src/services/sentry.ts +++ b/apps/api/src/services/sentry.ts @@ -11,6 +11,6 @@ if (process.env.SENTRY_DSN) { tracesSampleRate: process.env.SENTRY_ENVIRONMENT === "dev" ? 1.0 : 0.045, profilesSampleRate: 1.0, serverName: process.env.FLY_MACHINE_ID, - environment: process.env.SENTRY_ENVIRONMENT ?? "production" + environment: process.env.SENTRY_ENVIRONMENT ?? "production", }); } diff --git a/apps/api/src/services/supabase.ts b/apps/api/src/services/supabase.ts index 521a82ca..4ab63815 100644 --- a/apps/api/src/services/supabase.ts +++ b/apps/api/src/services/supabase.ts @@ -15,12 +15,12 @@ class SupabaseService { if (!useDbAuthentication) { // Warn the user that Authentication is disabled by setting the client to null logger.warn( - "Authentication is disabled. Supabase client will not be initialized." + "Authentication is disabled. Supabase client will not be initialized.", ); this.client = null; } else if (!supabaseUrl || !supabaseServiceToken) { logger.error( - "Supabase environment variables aren't configured correctly. Supabase client will not be initialized. Fix ENV configuration or disable DB authentication with USE_DB_AUTHENTICATION env variable" + "Supabase environment variables aren't configured correctly. Supabase client will not be initialized. Fix ENV configuration or disable DB authentication with USE_DB_AUTHENTICATION env variable", ); } else { this.client = createClient(supabaseUrl, supabaseServiceToken); @@ -52,6 +52,6 @@ export const supabase_service: SupabaseClient = new Proxy( } // Otherwise, delegate access to the Supabase client. return Reflect.get(client, prop, receiver); - } - } + }, + }, ) as unknown as SupabaseClient; diff --git a/apps/api/src/services/system-monitor.ts b/apps/api/src/services/system-monitor.ts index 4fa4c478..886de6ff 100644 --- a/apps/api/src/services/system-monitor.ts +++ b/apps/api/src/services/system-monitor.ts @@ -137,7 +137,7 @@ class SystemMonitor { } } catch (error) { logger.warn( - `Unable to read cpuset.cpus.effective, defaulting to OS CPUs: ${error}` + `Unable to read cpuset.cpus.effective, defaulting to OS CPUs: ${error}`, ); cpus = os.cpus().map((cpu, index) => index); } diff --git a/apps/api/src/services/webhook.ts b/apps/api/src/services/webhook.ts index dfee11f6..6b580a36 100644 --- a/apps/api/src/services/webhook.ts +++ b/apps/api/src/services/webhook.ts @@ -14,12 +14,12 @@ export const callWebhook = async ( specified?: z.infer, v1 = false, eventType: WebhookEventType = "crawl.page", - awaitWebhook: boolean = false + awaitWebhook: boolean = false, ) => { try { const selfHostedUrl = process.env.SELF_HOSTED_WEBHOOK_URL?.replace( "{{JOB_ID}}", - id + id, ); const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === "true"; let webhookUrl = @@ -36,7 +36,7 @@ export const callWebhook = async ( .limit(1); if (error) { logger.error( - `Error fetching webhook URL for team ID: ${teamId}, error: ${error.message}` + `Error fetching webhook URL for team ID: ${teamId}, error: ${error.message}`, ); return null; } @@ -54,7 +54,7 @@ export const callWebhook = async ( specified, v1, eventType, - awaitWebhook + awaitWebhook, }); if (!webhookUrl) { @@ -75,7 +75,7 @@ export const callWebhook = async ( dataToSend.push({ content: data.result.links[i].content.content, markdown: data.result.links[i].content.markdown, - metadata: data.result.links[i].content.metadata + metadata: data.result.links[i].content.metadata, }); } } @@ -98,19 +98,19 @@ export const callWebhook = async ( ? data?.error || undefined : eventType === "crawl.page" ? data?.error || undefined - : undefined + : undefined, }, { headers: { "Content-Type": "application/json", - ...webhookUrl.headers + ...webhookUrl.headers, }, - timeout: v1 ? 10000 : 30000 // 10 seconds timeout (v1) - } + timeout: v1 ? 10000 : 30000, // 10 seconds timeout (v1) + }, ); } catch (error) { logger.error( - `Axios error (0) sending webhook for team ID: ${teamId}, error: ${error.message}` + `Axios error (0) sending webhook for team ID: ${teamId}, error: ${error.message}`, ); } } else { @@ -130,24 +130,24 @@ export const callWebhook = async ( ? data?.error || undefined : eventType === "crawl.page" ? data?.error || undefined - : undefined + : undefined, }, { headers: { "Content-Type": "application/json", - ...webhookUrl.headers - } - } + ...webhookUrl.headers, + }, + }, ) .catch((error) => { logger.error( - `Axios error sending webhook for team ID: ${teamId}, error: ${error.message}` + `Axios error sending webhook for team ID: ${teamId}, error: ${error.message}`, ); }); } } catch (error) { logger.debug( - `Error sending webhook for team ID: ${teamId}, error: ${error.message}` + `Error sending webhook for team ID: ${teamId}, error: ${error.message}`, ); } }; diff --git a/apps/api/src/supabase_types.ts b/apps/api/src/supabase_types.ts index 8f9e1b64..00b2efbb 100644 --- a/apps/api/src/supabase_types.ts +++ b/apps/api/src/supabase_types.ts @@ -40,7 +40,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; company: { @@ -77,7 +77,7 @@ export interface Database { columns: ["pricing_plan_id"]; referencedRelation: "pricing_plan"; referencedColumns: ["id"]; - } + }, ]; }; constants: { @@ -126,7 +126,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; customers: { @@ -157,7 +157,7 @@ export interface Database { columns: ["user_id"]; referencedRelation: "users"; referencedColumns: ["id"]; - } + }, ]; }; data: { @@ -236,7 +236,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; data_partitioned: { @@ -390,7 +390,7 @@ export interface Database { columns: ["company_id"]; referencedRelation: "company"; referencedColumns: ["company_id"]; - } + }, ]; }; message: { @@ -439,7 +439,7 @@ export interface Database { columns: ["conversation_id"]; referencedRelation: "conversation"; referencedColumns: ["conversation_id"]; - } + }, ]; }; model_configuration: { @@ -479,7 +479,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; monthly_message_counts: { @@ -507,7 +507,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; prices: { @@ -560,7 +560,7 @@ export interface Database { columns: ["product_id"]; referencedRelation: "products"; referencedColumns: ["id"]; - } + }, ]; }; pricing_plan: { @@ -747,7 +747,7 @@ export interface Database { columns: ["user_id"]; referencedRelation: "users"; referencedColumns: ["id"]; - } + }, ]; }; suggested_questions: { @@ -775,7 +775,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; user_notifications: { @@ -821,7 +821,7 @@ export interface Database { columns: ["user_id"]; referencedRelation: "users"; referencedColumns: ["id"]; - } + }, ]; }; users: { @@ -864,7 +864,7 @@ export interface Database { columns: ["id"]; referencedRelation: "users"; referencedColumns: ["id"]; - } + }, ]; }; z_testcomp_92511: { @@ -934,7 +934,7 @@ export interface Database { columns: ["project_id"]; referencedRelation: "mendable_project"; referencedColumns: ["id"]; - } + }, ]; }; }; diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index cfae8f23..5325a0ad 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -3,7 +3,7 @@ import { AuthCreditUsageChunk, ScrapeOptions, Document as V1Document, - webhookSchema + webhookSchema, } from "./controllers/v1/types"; import { ExtractorOptions, Document } from "./lib/entities"; import { InternalOptions } from "./scraper/scrapeURL"; @@ -127,7 +127,7 @@ export enum RateLimiterMode { Scrape = "scrape", Preview = "preview", Search = "search", - Map = "map" + Map = "map", } export type AuthResponse = @@ -149,7 +149,7 @@ export enum NotificationType { LIMIT_REACHED = "limitReached", RATE_LIMIT_REACHED = "rateLimitReached", AUTO_RECHARGE_SUCCESS = "autoRechargeSuccess", - AUTO_RECHARGE_FAILED = "autoRechargeFailed" + AUTO_RECHARGE_FAILED = "autoRechargeFailed", } export type ScrapeLog = { From de57e7f4dd0e3e92d31609f6373620fffa6eaded Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 11 Dec 2024 20:07:05 -0300 Subject: [PATCH 24/52] Nick: from dependencies to dev-dependencies --- apps/api/package.json | 2 +- apps/api/pnpm-lock.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 1f4fd8a8..c4e70901 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -43,6 +43,7 @@ "jest-fetch-mock": "^3.0.3", "mammoth": "^1.7.2", "nodemon": "^2.0.20", + "prettier": "^3.4.2", "supabase": "^1.77.9", "supertest": "^6.3.3", "ts-jest": "^29.1.1", @@ -102,7 +103,6 @@ "pdf-parse": "^1.1.1", "pos": "^0.4.2", "posthog-node": "^4.0.1", - "prettier": "^3.4.2", "promptable": "^0.0.10", "puppeteer": "^22.12.1", "rate-limiter-flexible": "2.4.2", diff --git a/apps/api/pnpm-lock.yaml b/apps/api/pnpm-lock.yaml index 563965c1..6d971708 100644 --- a/apps/api/pnpm-lock.yaml +++ b/apps/api/pnpm-lock.yaml @@ -164,9 +164,6 @@ importers: posthog-node: specifier: ^4.0.1 version: 4.0.1 - prettier: - specifier: ^3.4.2 - version: 3.4.2 promptable: specifier: ^0.0.10 version: 0.0.10 @@ -282,6 +279,9 @@ importers: nodemon: specifier: ^2.0.20 version: 2.0.22 + prettier: + specifier: ^3.4.2 + version: 3.4.2 supabase: specifier: ^1.77.9 version: 1.172.2 From e22a0b596c3f1ee7d7537c0df5652de18715caf3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 12 Dec 2024 13:30:00 -0300 Subject: [PATCH 25/52] Nick: custom metadata --- apps/api/src/controllers/v1/types.ts | 1 + apps/api/src/services/webhook.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index 57e208b4..f9e57696 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -251,6 +251,7 @@ export const webhookSchema = z.preprocess( .object({ url: z.string().url(), headers: z.record(z.string(), z.string()).default({}), + metadata: z.record(z.string(), z.string()).default({}), }) .strict(strictMessage), ); diff --git a/apps/api/src/services/webhook.ts b/apps/api/src/services/webhook.ts index 6b580a36..d1381b05 100644 --- a/apps/api/src/services/webhook.ts +++ b/apps/api/src/services/webhook.ts @@ -99,6 +99,7 @@ export const callWebhook = async ( : eventType === "crawl.page" ? data?.error || undefined : undefined, + metadata: webhookUrl.metadata || undefined, }, { headers: { @@ -131,6 +132,7 @@ export const callWebhook = async ( : eventType === "crawl.page" ? data?.error || undefined : undefined, + metadata: webhookUrl.metadata || undefined, }, { headers: { From e06647b4b0b0f09f1febb4c703d0569611b75112 Mon Sep 17 00:00:00 2001 From: Eric Ciarla Date: Thu, 12 Dec 2024 14:41:11 -0500 Subject: [PATCH 26/52] Move full app examples to other repo --- .../automated_price_tracking/.env.example | 2 - .../.github/workflows/check_prices.yml | 33 ----- examples/automated_price_tracking/.gitignore | 1 - examples/automated_price_tracking/README.md | 31 ---- .../automated_price_tracking/check_prices.py | 49 ------- examples/automated_price_tracking/database.py | 134 ------------------ .../automated_price_tracking/notifications.py | 36 ----- .../automated_price_tracking/requirements.txt | 9 -- examples/automated_price_tracking/scraper.py | 38 ----- examples/automated_price_tracking/ui.py | 86 ----------- examples/automated_price_tracking/utils.py | 28 ---- 11 files changed, 447 deletions(-) delete mode 100644 examples/automated_price_tracking/.env.example delete mode 100644 examples/automated_price_tracking/.github/workflows/check_prices.yml delete mode 100644 examples/automated_price_tracking/.gitignore delete mode 100644 examples/automated_price_tracking/README.md delete mode 100644 examples/automated_price_tracking/check_prices.py delete mode 100644 examples/automated_price_tracking/database.py delete mode 100644 examples/automated_price_tracking/notifications.py delete mode 100644 examples/automated_price_tracking/requirements.txt delete mode 100644 examples/automated_price_tracking/scraper.py delete mode 100644 examples/automated_price_tracking/ui.py delete mode 100644 examples/automated_price_tracking/utils.py diff --git a/examples/automated_price_tracking/.env.example b/examples/automated_price_tracking/.env.example deleted file mode 100644 index 4a9dbf9a..00000000 --- a/examples/automated_price_tracking/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -FIRECRAWL_API_KEY= -POSTGRES_URL= \ No newline at end of file diff --git a/examples/automated_price_tracking/.github/workflows/check_prices.yml b/examples/automated_price_tracking/.github/workflows/check_prices.yml deleted file mode 100644 index 5bd0e671..00000000 --- a/examples/automated_price_tracking/.github/workflows/check_prices.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Price Check - -on: - schedule: - # Runs every 6 hours - - cron: "0 0,6,12,18 * * *" - workflow_dispatch: # Allows manual triggering - -jobs: - check-prices: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - cache: "pip" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run price checker - env: - FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }} - POSTGRES_URL: ${{ secrets.POSTGRES_URL }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - run: python check_prices.py diff --git a/examples/automated_price_tracking/.gitignore b/examples/automated_price_tracking/.gitignore deleted file mode 100644 index 1d17dae1..00000000 --- a/examples/automated_price_tracking/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.venv diff --git a/examples/automated_price_tracking/README.md b/examples/automated_price_tracking/README.md deleted file mode 100644 index 9ab50dbe..00000000 --- a/examples/automated_price_tracking/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Automated Price Tracking System - -A robust price tracking system that monitors product prices across e-commerce websites and notifies users of price changes through Discord. - -## Features - -- Automated price checking every 6 hours -- Support for multiple e-commerce platforms through Firecrawl API -- Discord notifications for price changes -- Historical price data storage in PostgreSQL database -- Interactive price history visualization with Streamlit - -## Setup - -1. Clone the repository -2. Install dependencies: - - ```bash - pip install -r requirements.txt - ``` - -3. Configure environment variables: - - ```bash - cp .env.example .env - ``` - - Then edit `.env` with your: - - Discord webhook URL - - Database credentials - - Firecrawl API key diff --git a/examples/automated_price_tracking/check_prices.py b/examples/automated_price_tracking/check_prices.py deleted file mode 100644 index 33a48843..00000000 --- a/examples/automated_price_tracking/check_prices.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import asyncio -from database import Database -from dotenv import load_dotenv -from firecrawl import FirecrawlApp -from scraper import scrape_product -from notifications import send_price_alert - -load_dotenv() - -db = Database(os.getenv("POSTGRES_URL")) -app = FirecrawlApp() - -# Threshold percentage for price drop alerts (e.g., 5% = 0.05) -PRICE_DROP_THRESHOLD = 0.05 - - -async def check_prices(): - products = db.get_all_products() - product_urls = set(product.url for product in products) - - for product_url in product_urls: - # Get the price history - price_history = db.get_price_history(product_url) - if not price_history: - continue - - # Get the earliest recorded price - earliest_price = price_history[-1].price - - # Retrieve updated product data - updated_product = scrape_product(product_url) - current_price = updated_product["price"] - - # Add the price to the database - db.add_price(updated_product) - print(f"Added new price entry for {updated_product['name']}") - - # Check if price dropped below threshold - if earliest_price > 0: # Avoid division by zero - price_drop = (earliest_price - current_price) / earliest_price - if price_drop >= PRICE_DROP_THRESHOLD: - await send_price_alert( - updated_product["name"], earliest_price, current_price, product_url - ) - - -if __name__ == "__main__": - asyncio.run(check_prices()) diff --git a/examples/automated_price_tracking/database.py b/examples/automated_price_tracking/database.py deleted file mode 100644 index 2aec92a8..00000000 --- a/examples/automated_price_tracking/database.py +++ /dev/null @@ -1,134 +0,0 @@ -from sqlalchemy import create_engine, Column, String, Float, DateTime, ForeignKey -from sqlalchemy.orm import sessionmaker, relationship, declarative_base -from datetime import datetime - -Base = declarative_base() - - -class Product(Base): - __tablename__ = "products" - - url = Column(String, primary_key=True) - prices = relationship( - "PriceHistory", back_populates="product", cascade="all, delete-orphan" - ) - - -class PriceHistory(Base): - __tablename__ = "price_histories" - - id = Column(String, primary_key=True) - product_url = Column(String, ForeignKey("products.url")) - name = Column(String, nullable=False) - price = Column(Float, nullable=False) - currency = Column(String, nullable=False) - main_image_url = Column(String) - timestamp = Column(DateTime, nullable=False) - product = relationship("Product", back_populates="prices") - - -class Database: - def __init__(self, connection_string): - self.engine = create_engine(connection_string) - Base.metadata.create_all(self.engine) - self.Session = sessionmaker(bind=self.engine) - - def add_product(self, url): - session = self.Session() - try: - # Create the product entry - product = Product(url=url) - session.merge(product) # merge will update if exists, insert if not - session.commit() - finally: - session.close() - - def product_exists(self, url): - session = self.Session() - try: - return session.query(Product).filter(Product.url == url).first() is not None - finally: - session.close() - - def add_price(self, product_data): - session = self.Session() - try: - # First ensure the product exists - if not self.product_exists(product_data["url"]): - # Create the product if it doesn't exist - product = Product(url=product_data["url"]) - session.add(product) - session.flush() # Flush to ensure the product is created before adding price - - # Convert timestamp string to datetime if it's a string - timestamp = product_data["timestamp"] - if isinstance(timestamp, str): - timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H-%M") - - price_history = PriceHistory( - id=f"{product_data['url']}_{timestamp.strftime('%Y%m%d%H%M%S')}", - product_url=product_data["url"], - name=product_data["name"], - price=product_data["price"], - currency=product_data["currency"], - main_image_url=product_data["main_image_url"], - timestamp=timestamp, - ) - session.add(price_history) - session.commit() - finally: - session.close() - - def get_all_products(self): - session = self.Session() - try: - return session.query(Product).all() - finally: - session.close() - - def get_price_history(self, url): - """Get price history for a product""" - session = self.Session() - try: - return ( - session.query(PriceHistory) - .filter(PriceHistory.product_url == url) - .order_by(PriceHistory.timestamp.desc()) - .all() - ) - finally: - session.close() - - def remove_all_products(self): - session = self.Session() - try: - # First delete all price histories - session.query(PriceHistory).delete() - # Then delete all products - session.query(Product).delete() - session.commit() - finally: - session.close() - - # def remove_product(self, url): - # """Remove a product and its price history""" - # session = self.Session() - # try: - # product = session.query(Product).filter(Product.url == url).first() - # if product: - # session.delete( - # product - # ) # This will also delete associated price history due to cascade - # session.commit() - # finally: - # session.close() - - -if __name__ == "__main__": - from dotenv import load_dotenv - import os - - load_dotenv() - - db = Database(os.getenv("POSTGRES_URL")) - db.remove_all_products() diff --git a/examples/automated_price_tracking/notifications.py b/examples/automated_price_tracking/notifications.py deleted file mode 100644 index 2837fb70..00000000 --- a/examples/automated_price_tracking/notifications.py +++ /dev/null @@ -1,36 +0,0 @@ -from dotenv import load_dotenv -import os -import aiohttp -import asyncio - -load_dotenv() - - -async def send_price_alert( - product_name: str, old_price: float, new_price: float, url: str -): - """Send a price drop alert to Discord""" - drop_percentage = ((old_price - new_price) / old_price) * 100 - - message = { - "embeds": [ - { - "title": "Price Drop Alert! 🎉", - "description": f"**{product_name}**\nPrice dropped by {drop_percentage:.1f}%!\n" - f"Old price: ${old_price:.2f}\n" - f"New price: ${new_price:.2f}\n" - f"[View Product]({url})", - "color": 3066993, - } - ] - } - - try: - async with aiohttp.ClientSession() as session: - await session.post(os.getenv("DISCORD_WEBHOOK_URL"), json=message) - except Exception as e: - print(f"Error sending Discord notification: {e}") - - -if __name__ == "__main__": - asyncio.run(send_price_alert("Test Product", 100, 90, "https://www.google.com")) diff --git a/examples/automated_price_tracking/requirements.txt b/examples/automated_price_tracking/requirements.txt deleted file mode 100644 index 52f0541b..00000000 --- a/examples/automated_price_tracking/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -streamlit -firecrawl-py -pydantic -psycopg2-binary -python-dotenv -sqlalchemy==2.0.35 -pandas -plotly -aiohttp \ No newline at end of file diff --git a/examples/automated_price_tracking/scraper.py b/examples/automated_price_tracking/scraper.py deleted file mode 100644 index fc06b73e..00000000 --- a/examples/automated_price_tracking/scraper.py +++ /dev/null @@ -1,38 +0,0 @@ -from firecrawl import FirecrawlApp -from pydantic import BaseModel, Field -from datetime import datetime -from dotenv import load_dotenv - -load_dotenv() -app = FirecrawlApp() - - -class Product(BaseModel): - """Schema for creating a new product""" - - url: str = Field(description="The URL of the product") - name: str = Field(description="The product name/title") - price: float = Field(description="The current price of the product") - currency: str = Field(description="Currency code (USD, EUR, etc)") - main_image_url: str = Field(description="The URL of the main image of the product") - - -def scrape_product(url: str): - extracted_data = app.scrape_url( - url, - params={ - "formats": ["extract"], - "extract": {"schema": Product.model_json_schema()}, - }, - ) - - # Add the scraping date to the extracted data - extracted_data["extract"]["timestamp"] = datetime.utcnow() - - return extracted_data["extract"] - - -if __name__ == "__main__": - product = "https://www.amazon.com/gp/product/B002U21ZZK/" - - print(scrape_product(product)) diff --git a/examples/automated_price_tracking/ui.py b/examples/automated_price_tracking/ui.py deleted file mode 100644 index 11969897..00000000 --- a/examples/automated_price_tracking/ui.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -import streamlit as st -import pandas as pd -import plotly.express as px - -from utils import is_valid_url -from database import Database -from dotenv import load_dotenv -from scraper import scrape_product - -load_dotenv() - -st.set_page_config(page_title="Price Tracker", page_icon="📊", layout="wide") - -with st.spinner("Loading database..."): - db = Database(os.getenv("POSTGRES_URL")) - - -# Set up sidebar -with st.sidebar: - st.title("Add New Product") - product_url = st.text_input("Product URL") - add_button = st.button("Add Product") - - if add_button: - if not product_url: - st.error("Please enter a product URL") - elif not is_valid_url(product_url): - st.error("Please enter a valid URL") - else: - db.add_product(product_url) - with st.spinner("Added product to database. Scraping product data..."): - product_data = scrape_product(product_url) - db.add_price(product_data) - st.success("Product is now being tracked!") - -# Main content -st.title("Price Tracker Dashboard") -st.markdown("## Tracked Products") - -# Get all products and their price histories -products = db.get_all_products() - -# Create a card for each product -for product in products: - price_history = db.get_price_history(product.url) - if price_history: - # Create DataFrame for plotting - df = pd.DataFrame( - [ - {"timestamp": ph.timestamp, "price": ph.price, "name": ph.name} - for ph in price_history - ] - ) - - # Create a card-like container for each product - with st.expander(df["name"][0], expanded=False): - st.markdown("---") - col1, col2 = st.columns([1, 3]) - - with col1: - if price_history[0].main_image_url: - st.image(price_history[0].main_image_url, width=200) - st.metric( - label="Current Price", - value=f"{price_history[0].price} {price_history[0].currency}", - ) - - with col2: - # Create price history plot - fig = px.line( - df, - x="timestamp", - y="price", - title=None, - ) - fig.update_layout( - xaxis_title=None, - yaxis_title="Price ($)", - showlegend=False, - margin=dict(l=0, r=0, t=0, b=0), - height=300, - ) - fig.update_xaxes(tickformat="%Y-%m-%d %H:%M", tickangle=45) - fig.update_yaxes(tickprefix="$", tickformat=".2f") - st.plotly_chart(fig, use_container_width=True) diff --git a/examples/automated_price_tracking/utils.py b/examples/automated_price_tracking/utils.py deleted file mode 100644 index c7af0a94..00000000 --- a/examples/automated_price_tracking/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -from urllib.parse import urlparse -import re - - -def is_valid_url(url: str) -> bool: - try: - # Parse the URL - result = urlparse(url) - - # Check if scheme and netloc are present - if not all([result.scheme, result.netloc]): - return False - - # Check if scheme is http or https - if result.scheme not in ["http", "https"]: - return False - - # Basic regex pattern for domain validation - domain_pattern = ( - r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$" - ) - if not re.match(domain_pattern, result.netloc): - return False - - return True - - except Exception: - return False From a2998d4499965be88f8b4650ef1d0570376987cd Mon Sep 17 00:00:00 2001 From: Eric Ciarla Date: Thu, 12 Dec 2024 16:10:10 -0500 Subject: [PATCH 27/52] Hash Urls --- apps/api/src/routes/v1.ts | 2 +- .../src/scraper/WebScraper/utils/blocklist.ts | 65 +++++++++++++------ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/apps/api/src/routes/v1.ts b/apps/api/src/routes/v1.ts index 5daa077b..f09573d9 100644 --- a/apps/api/src/routes/v1.ts +++ b/apps/api/src/routes/v1.ts @@ -123,7 +123,7 @@ function blocklistMiddleware(req: Request, res: Response, next: NextFunction) { return res.status(403).json({ success: false, error: - "URL is blocked intentionally. Firecrawl currently does not support social media scraping due to policy restrictions.", + "URL is blocked intentionally. Firecrawl currently does not support scraping this site due to policy restrictions.", }); } } diff --git a/apps/api/src/scraper/WebScraper/utils/blocklist.ts b/apps/api/src/scraper/WebScraper/utils/blocklist.ts index 58fcade4..0a3ef705 100644 --- a/apps/api/src/scraper/WebScraper/utils/blocklist.ts +++ b/apps/api/src/scraper/WebScraper/utils/blocklist.ts @@ -1,26 +1,51 @@ import { logger } from "../../../lib/logger"; +import crypto from "crypto"; +import { configDotenv } from "dotenv"; +configDotenv(); -const socialMediaBlocklist = [ - "facebook.com", - "x.com", - "twitter.com", - "instagram.com", - "linkedin.com", - "snapchat.com", - "tiktok.com", - "reddit.com", - "tumblr.com", - "flickr.com", - "whatsapp.com", - "wechat.com", - "telegram.org", - "researchhub.com", - "youtube.com", - "corterix.com", - "southwest.com", - "ryanair.com", +const hashKey = Buffer.from(process.env.HASH_KEY || "", "utf-8"); +const algorithm = "aes-256-ecb"; + +function decryptAES(ciphertext: string, key: Buffer): string { + const decipher = crypto.createDecipheriv(algorithm, key, null); + const decrypted = Buffer.concat([ + decipher.update(Buffer.from(ciphertext, "base64")), + decipher.final(), + ]); + return decrypted.toString("utf-8"); +} + +const urlBlocklist = [ + "h8ngAFXUNLO3ZqQufJjGVA==", + "fEGiDm/TWDBkXUXejFVICg==", + "l6Mei7IGbEmTTFoSudUnqQ==", + "4OjallJzXRiZUAWDiC2Xww==", + "ReSvkSfx34TNEdecmmSDdQ==", + "X1E4WtdmXAv3SAX9xN925Q==", + "VTzBQfMtXZzM05mnNkWkjA==", + "m/q4Lb2Z8cxwU7/CoztOFg==", + "UbVnmRaeG+gKcyVDLAm0vg==", + "xNQhczYG22tTVc6lYE3qwg==", + "CQfGDydbg4l1swRCru6O6Q==", + "l86LQxm2NonTWMauXwEsPw==", + "6v4QDUcwjnID80G+uU+tgw==", + "pCF/6nrKZAxaYntzEGluZQ==", + "r0CRhAmQqSe7V2s3073T00sAh4WcS5779jwuGJ26ows==", + "aBOVqRFBM4UVg33usY10NdiF0HCnFH/ImtD0n+zIpc8==", + "QV436UZuQ6D0Dqrx9MwaGw==", + "OYVvrwILYbzA2mSSqOPPpw==", + "xW2i4C0Dzcnp+qu12u0SAw==", + "OLHba209l0dfl0MI4EnQonBITK9z8Qwgd/NsuaTkXmA=", + "X0VynmNjpL3PrYxpUIG7sFMBt8OlrmQWtxj8oXVu2QM=", + "ObdlM5NEkvBJ/sojRW5K/Q==", + "C8Th38X0SjsE1vL/OsD8bA==", + "PTbGg8PK/h0Seyw4HEpK4Q==", + "lZdQMknjHb7+4+sjF3qNTw==", + "LsgSq54q5oDysbva29JxnQ==", ]; +const decryptedBlocklist = hashKey.length > 0 ? urlBlocklist.map((ciphertext) => decryptAES(ciphertext, hashKey)) : []; + const allowedKeywords = [ "pulse", "privacy", @@ -65,7 +90,7 @@ export function isUrlBlocked(url: string): boolean { const hostname = urlObj.hostname.toLowerCase(); // Check if the URL matches any domain in the blocklist - const isBlocked = socialMediaBlocklist.some((domain) => { + const isBlocked = decryptedBlocklist.some((domain) => { const domainPattern = new RegExp( `(^|\\.)${domain.replace(".", "\\.")}(\\.|$)`, "i", From 3b0d192d1b25985097f436c03c89744a2d329410 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 12 Dec 2024 18:14:11 -0300 Subject: [PATCH 28/52] Update types.ts --- apps/api/src/controllers/v1/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index f9e57696..076d8b0b 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -197,7 +197,7 @@ export const extractV1Options = z limit: z.number().int().positive().finite().safe().optional(), ignoreSitemap: z.boolean().default(false), includeSubdomains: z.boolean().default(true), - allowExternalLinks: z.boolean().default(false), + allowExternalLinks: z.boolean().default(true), origin: z.string().optional().default("api"), timeout: z.number().int().positive().finite().safe().default(60000), }) From 13afe4c73346f56fa24252094d55af1188b918be Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 12 Dec 2024 21:52:20 -0300 Subject: [PATCH 29/52] Update index.ts --- apps/js-sdk/firecrawl/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/js-sdk/firecrawl/src/index.ts b/apps/js-sdk/firecrawl/src/index.ts index eb402b4d..37fc5ef0 100644 --- a/apps/js-sdk/firecrawl/src/index.ts +++ b/apps/js-sdk/firecrawl/src/index.ts @@ -157,6 +157,7 @@ export interface CrawlParams { webhook?: string | { url: string; headers?: Record; + metadata?: Record; }; deduplicateSimilarURLs?: boolean; ignoreQueryParameters?: boolean; From 6b17a53d4bc4386b2dfd583cf7206fa72b9b95e3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 12 Dec 2024 21:53:15 -0300 Subject: [PATCH 30/52] Update package.json --- apps/js-sdk/firecrawl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/js-sdk/firecrawl/package.json b/apps/js-sdk/firecrawl/package.json index 73224dc4..30277cc3 100644 --- a/apps/js-sdk/firecrawl/package.json +++ b/apps/js-sdk/firecrawl/package.json @@ -1,6 +1,6 @@ { "name": "@mendable/firecrawl-js", - "version": "1.9.2", + "version": "1.9.3", "description": "JavaScript SDK for Firecrawl API", "main": "dist/index.js", "types": "dist/index.d.ts", From e74e4bcefc5ebf97ef8fbe726c21e924bfef7b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Fri, 13 Dec 2024 23:46:33 +0100 Subject: [PATCH 31/52] feat(runWebScraper): retry a scrape max 3 times in a crawl if the status code is failure --- apps/api/logview.js | 16 +- apps/api/src/controllers/v0/scrape.ts | 16 +- apps/api/src/controllers/v1/extract.ts | 4 +- apps/api/src/controllers/v1/types.ts | 2 +- apps/api/src/main/runWebScraper.ts | 139 ++++++++++-------- .../scraper/scrapeURL/lib/extractMetadata.ts | 2 +- apps/api/src/types.ts | 1 + 7 files changed, 108 insertions(+), 72 deletions(-) diff --git a/apps/api/logview.js b/apps/api/logview.js index 232d2cda..3c0db523 100644 --- a/apps/api/logview.js +++ b/apps/api/logview.js @@ -1,7 +1,19 @@ const fs = require("fs"); -const logs = fs.readFileSync("7a373219-0eb4-4e47-b2df-e90e12afd5c1.log", "utf8") - .split("\n").filter(x => x.trim().length > 0).map(x => JSON.parse(x)); +// METHOD: Winston log file +// const logs = fs.readFileSync("7a373219-0eb4-4e47-b2df-e90e12afd5c1.log", "utf8") +// .split("\n").filter(x => x.trim().length > 0).map(x => JSON.parse(x)); + +// METHOD: GCloud export +const logs = [ + "downloaded-logs-20241213-225607.json", + "downloaded-logs-20241213-225654.json", + "downloaded-logs-20241213-225720.json", + "downloaded-logs-20241213-225758.json", + "downloaded-logs-20241213-225825.json", + "downloaded-logs-20241213-225843.json", +].flatMap(x => JSON.parse(fs.readFileSync(x, "utf8"))).map(x => x.jsonPayload); + const crawlIds = [...new Set(logs.map(x => x.crawlId).filter(x => x))]; diff --git a/apps/api/src/controllers/v0/scrape.ts b/apps/api/src/controllers/v0/scrape.ts index 8501e502..96e6ea4f 100644 --- a/apps/api/src/controllers/v0/scrape.ts +++ b/apps/api/src/controllers/v0/scrape.ts @@ -8,7 +8,6 @@ import { authenticateUser } from "../auth"; import { PlanType, RateLimiterMode } from "../../types"; import { logJob } from "../../services/logging/log_job"; import { - Document, fromLegacyCombo, toLegacyDocument, url as urlSchema, @@ -29,6 +28,7 @@ import * as Sentry from "@sentry/node"; import { getJobPriority } from "../../lib/job-priority"; import { fromLegacyScrapeOptions } from "../v1/types"; import { ZodError } from "zod"; +import { Document as V0Document } from "./../../lib/entities"; export async function scrapeHelper( jobId: string, @@ -42,7 +42,7 @@ export async function scrapeHelper( ): Promise<{ success: boolean; error?: string; - data?: Document | { url: string }; + data?: V0Document | { url: string }; returnCode: number; }> { const url = urlSchema.parse(req.body.url); @@ -241,9 +241,9 @@ export async function scrapeController(req: Request, res: Response) { const endTime = new Date().getTime(); const timeTakenInSeconds = (endTime - startTime) / 1000; const numTokens = - result.data && (result.data as Document).markdown + result.data && (result.data as V0Document).markdown ? numTokensFromString( - (result.data as Document).markdown!, + (result.data as V0Document).markdown!, "gpt-3.5-turbo", ) : 0; @@ -276,14 +276,14 @@ export async function scrapeController(req: Request, res: Response) { let doc = result.data; if (!pageOptions || !pageOptions.includeRawHtml) { - if (doc && (doc as Document).rawHtml) { - delete (doc as Document).rawHtml; + if (doc && (doc as V0Document).rawHtml) { + delete (doc as V0Document).rawHtml; } } if (pageOptions && pageOptions.includeExtract) { - if (!pageOptions.includeMarkdown && doc && (doc as Document).markdown) { - delete (doc as Document).markdown; + if (!pageOptions.includeMarkdown && doc && (doc as V0Document).markdown) { + delete (doc as V0Document).markdown; } } diff --git a/apps/api/src/controllers/v1/extract.ts b/apps/api/src/controllers/v1/extract.ts index 0c286253..d05dbf6e 100644 --- a/apps/api/src/controllers/v1/extract.ts +++ b/apps/api/src/controllers/v1/extract.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { - // Document, + Document, RequestWithAuth, ExtractRequest, extractRequestSchema, @@ -8,7 +8,7 @@ import { MapDocument, scrapeOptions, } from "./types"; -import { Document } from "../../lib/entities"; +// import { Document } from "../../lib/entities"; import Redis from "ioredis"; import { configDotenv } from "dotenv"; import { performRanking } from "../../lib/ranker"; diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index 076d8b0b..d3f110c8 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -396,7 +396,7 @@ export type Document = { articleSection?: string; url?: string; sourceURL?: string; - statusCode?: number; + statusCode: number; error?: string; [key: string]: string | string[] | number | undefined; }; diff --git a/apps/api/src/main/runWebScraper.ts b/apps/api/src/main/runWebScraper.ts index dc907371..411acfe6 100644 --- a/apps/api/src/main/runWebScraper.ts +++ b/apps/api/src/main/runWebScraper.ts @@ -49,6 +49,7 @@ export async function startWebScraperPipeline({ bull_job_id: job.id.toString(), priority: job.opts.priority, is_scrape: job.data.is_scrape ?? false, + is_crawl: !!(job.data.crawl_id && job.data.crawlerOptions !== null), }); } @@ -63,73 +64,63 @@ export async function runWebScraper({ bull_job_id, priority, is_scrape = false, + is_crawl = false, }: RunWebScraperParams): Promise { + const tries = is_crawl ? 3 : 1; + let response: ScrapeUrlResponse | undefined = undefined; let engines: EngineResultsTracker = {}; - try { - response = await scrapeURL(bull_job_id, url, scrapeOptions, { - priority, - ...internalOptions, - }); - if (!response.success) { - if (response.error instanceof Error) { - throw response.error; - } else { - throw new Error( - "scrapeURL error: " + - (Array.isArray(response.error) - ? JSON.stringify(response.error) - : typeof response.error === "object" - ? JSON.stringify({ ...response.error }) - : response.error), - ); - } + let error: any = undefined; + + for (let i = 0; i < tries; i++) { + if (i > 0) { + logger.debug("Retrying scrape...", { scrapeId: bull_job_id, jobId: bull_job_id, method: "runWebScraper", module: "runWebScraper", tries, i, previousStatusCode: (response as any)?.document?.metadata?.statusCode, previousError: error }); } - if (is_scrape === false) { - let creditsToBeBilled = 1; // Assuming 1 credit per document - if (scrapeOptions.extract) { - creditsToBeBilled = 5; - } + response = undefined; + engines = {}; + error = undefined; - billTeam(team_id, undefined, creditsToBeBilled).catch((error) => { - logger.error( - `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`, - ); - // Optionally, you could notify an admin or add to a retry queue here + try { + response = await scrapeURL(bull_job_id, url, scrapeOptions, { + priority, + ...internalOptions, }); + if (!response.success) { + if (response.error instanceof Error) { + throw response.error; + } else { + throw new Error( + "scrapeURL error: " + + (Array.isArray(response.error) + ? JSON.stringify(response.error) + : typeof response.error === "object" + ? JSON.stringify({ ...response.error }) + : response.error), + ); + } + } + + // This is where the returnvalue from the job is set + // onSuccess(response.document, mode); + + engines = response.engines; + + if ((response.document.metadata.statusCode >= 200 && response.document.metadata.statusCode < 300) || response.document.metadata.statusCode === 304) { + // status code is good -- do not attempt retry + break; + } + } catch (error) { + engines = + response !== undefined + ? response.engines + : typeof error === "object" && error !== null + ? ((error as any).results ?? {}) + : {}; } + } - // This is where the returnvalue from the job is set - // onSuccess(response.document, mode); - - engines = response.engines; - return response; - } catch (error) { - engines = - response !== undefined - ? response.engines - : typeof error === "object" && error !== null - ? ((error as any).results ?? {}) - : {}; - - if (response !== undefined) { - return { - ...response, - success: false, - error, - }; - } else { - return { - success: false, - error, - logs: ["no logs -- error coming from runWebScraper"], - engines, - }; - } - // onError(error); - } finally { - const engineOrder = Object.entries(engines) + const engineOrder = Object.entries(engines) .sort((a, b) => a[1].startedAt - b[1].startedAt) .map((x) => x[0]) as Engine[]; @@ -158,6 +149,38 @@ export async function runWebScraper({ }, }); } + + if (error === undefined && response?.success) { + if (is_scrape === false) { + let creditsToBeBilled = 1; // Assuming 1 credit per document + if (scrapeOptions.extract) { + creditsToBeBilled = 5; + } + + billTeam(team_id, undefined, creditsToBeBilled).catch((error) => { + logger.error( + `Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`, + ); + // Optionally, you could notify an admin or add to a retry queue here + }); + } + + return response; + } else { + if (response !== undefined) { + return { + ...response, + success: false, + error, + }; + } else { + return { + success: false, + error, + logs: ["no logs -- error coming from runWebScraper"], + engines, + }; + } } } diff --git a/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts b/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts index 040bf0ee..c67f9cbd 100644 --- a/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts +++ b/apps/api/src/scraper/scrapeURL/lib/extractMetadata.ts @@ -5,7 +5,7 @@ import { Meta } from ".."; export function extractMetadata( meta: Meta, html: string, -): Document["metadata"] { +): Partial { let title: string | undefined = undefined; let description: string | undefined = undefined; let language: string | undefined = undefined; diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index 5325a0ad..9db79bc5 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -55,6 +55,7 @@ export interface RunWebScraperParams { bull_job_id: string; priority?: number; is_scrape?: boolean; + is_crawl?: boolean; } export type RunWebScraperResult = From 4b5014d7fe1336129f91e97b99a0fd495a4e019b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sat, 14 Dec 2024 01:11:43 +0100 Subject: [PATCH 32/52] feat(v1/batch/scrape): add ignoreInvalidURLs option --- apps/api/src/controllers/v1/batch-scrape.ts | 40 +++++++++++++++++---- apps/api/src/controllers/v1/types.ts | 34 ++++++++++++++++++ apps/api/src/lib/crawl-redis.ts | 4 +++ apps/api/src/services/queue-jobs.ts | 1 + 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/apps/api/src/controllers/v1/batch-scrape.ts b/apps/api/src/controllers/v1/batch-scrape.ts index 89fa6741..19ce3ba0 100644 --- a/apps/api/src/controllers/v1/batch-scrape.ts +++ b/apps/api/src/controllers/v1/batch-scrape.ts @@ -3,9 +3,11 @@ import { v4 as uuidv4 } from "uuid"; import { BatchScrapeRequest, batchScrapeRequestSchema, - CrawlResponse, + batchScrapeRequestSchemaNoURLValidation, + url as urlSchema, RequestWithAuth, ScrapeOptions, + BatchScrapeResponse, } from "./types"; import { addCrawlJobs, @@ -21,10 +23,14 @@ import { callWebhook } from "../../services/webhook"; import { logger as _logger } from "../../lib/logger"; export async function batchScrapeController( - req: RequestWithAuth<{}, CrawlResponse, BatchScrapeRequest>, - res: Response, + req: RequestWithAuth<{}, BatchScrapeResponse, BatchScrapeRequest>, + res: Response, ) { - req.body = batchScrapeRequestSchema.parse(req.body); + if (req.body?.ignoreInvalidURLs === true) { + req.body = batchScrapeRequestSchemaNoURLValidation.parse(req.body); + } else { + req.body = batchScrapeRequestSchema.parse(req.body); + } const id = req.body.appendToId ?? uuidv4(); const logger = _logger.child({ @@ -35,8 +41,27 @@ export async function batchScrapeController( teamId: req.auth.team_id, plan: req.auth.plan, }); + + let urls = req.body.urls; + let invalidURLs: string[] | undefined = undefined; + + if (req.body.ignoreInvalidURLs) { + invalidURLs = []; + + let pendingURLs = urls; + urls = []; + for (const u of pendingURLs) { + try { + const nu = urlSchema.parse(u); + urls.push(nu); + } catch (_) { + invalidURLs.push(u); + } + } + } + logger.debug("Batch scrape " + id + " starting", { - urlsLength: req.body.urls, + urlsLength: urls, appendToId: req.body.appendToId, account: req.account, }); @@ -70,7 +95,7 @@ export async function batchScrapeController( // If it is over 1000, we need to get the job priority, // otherwise we can use the default priority of 20 - if (req.body.urls.length > 1000) { + if (urls.length > 1000) { // set base to 21 jobPriority = await getJobPriority({ plan: req.auth.plan, @@ -84,7 +109,7 @@ export async function batchScrapeController( delete (scrapeOptions as any).urls; delete (scrapeOptions as any).appendToId; - const jobs = req.body.urls.map((x) => { + const jobs = urls.map((x) => { return { data: { url: x, @@ -140,5 +165,6 @@ export async function batchScrapeController( success: true, id, url: `${protocol}://${req.get("host")}/v1/batch/scrape/${id}`, + invalidURLs, }); } diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index d3f110c8..f7226338 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -262,6 +262,31 @@ export const batchScrapeRequestSchema = scrapeOptions origin: z.string().optional().default("api"), webhook: webhookSchema.optional(), appendToId: z.string().uuid().optional(), + ignoreInvalidURLs: z.boolean().default(false), + }) + .strict(strictMessage) + .refine( + (obj) => { + const hasExtractFormat = obj.formats?.includes("extract"); + const hasExtractOptions = obj.extract !== undefined; + return ( + (hasExtractFormat && hasExtractOptions) || + (!hasExtractFormat && !hasExtractOptions) + ); + }, + { + message: + "When 'extract' format is specified, 'extract' options must be provided, and vice versa", + }, + ); + +export const batchScrapeRequestSchemaNoURLValidation = scrapeOptions + .extend({ + urls: z.string().array(), + origin: z.string().optional().default("api"), + webhook: webhookSchema.optional(), + appendToId: z.string().uuid().optional(), + ignoreInvalidURLs: z.boolean().default(false), }) .strict(strictMessage) .refine( @@ -446,6 +471,15 @@ export type CrawlResponse = url: string; }; +export type BatchScrapeResponse = + | ErrorResponse + | { + success: true; + id: string; + url: string; + invalidURLs?: string[]; + }; + export type MapResponse = | ErrorResponse | { diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index 6ccb9436..3fcd9f67 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -60,6 +60,8 @@ export async function addCrawlJob(id: string, job_id: string) { } export async function addCrawlJobs(id: string, job_ids: string[]) { + if (job_ids.length === 0) return true; + _logger.debug("Adding crawl jobs to Redis...", { jobIds: job_ids, module: "crawl-redis", @@ -261,6 +263,8 @@ export async function lockURLs( sc: StoredCrawl, urls: string[], ): Promise { + if (urls.length === 0) return true; + urls = urls.map((url) => normalizeURL(url, sc)); const logger = _logger.child({ crawlId: id, diff --git a/apps/api/src/services/queue-jobs.ts b/apps/api/src/services/queue-jobs.ts index bd2b9121..ee9e6177 100644 --- a/apps/api/src/services/queue-jobs.ts +++ b/apps/api/src/services/queue-jobs.ts @@ -108,6 +108,7 @@ export async function addScrapeJobs( }; }[], ) { + if (jobs.length === 0) return true; // TODO: better await Promise.all( jobs.map((job) => From 9cc6576571477e28504c9cdb906e8922d8773c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sat, 14 Dec 2024 01:16:09 +0100 Subject: [PATCH 33/52] feat(js-sdk/batch/scrape): add ignoreInvalidURLs option --- apps/js-sdk/firecrawl/src/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/js-sdk/firecrawl/src/index.ts b/apps/js-sdk/firecrawl/src/index.ts index 37fc5ef0..020a2293 100644 --- a/apps/js-sdk/firecrawl/src/index.ts +++ b/apps/js-sdk/firecrawl/src/index.ts @@ -183,6 +183,7 @@ export interface BatchScrapeResponse { url?: string; success: true; error?: string; + invalidURLs?: string[]; } /** @@ -576,9 +577,10 @@ export default class FirecrawlApp { pollInterval: number = 2, idempotencyKey?: string, webhook?: CrawlParams["webhook"], + ignoreInvalidURLs?: boolean, ): Promise { const headers = this.prepareHeaders(idempotencyKey); - let jsonData: any = { urls, ...params }; + let jsonData: any = { urls, webhook, ignoreInvalidURLs, ...params }; if (jsonData?.extract?.schema) { let schema = jsonData.extract.schema; @@ -621,10 +623,12 @@ export default class FirecrawlApp { async asyncBatchScrapeUrls( urls: string[], params?: ScrapeParams, - idempotencyKey?: string + idempotencyKey?: string, + webhook?: CrawlParams["webhook"], + ignoreInvalidURLs?: boolean, ): Promise { const headers = this.prepareHeaders(idempotencyKey); - let jsonData: any = { urls, ...(params ?? {}) }; + let jsonData: any = { urls, webhook, ignoreInvalidURLs, ...(params ?? {}) }; try { const response: AxiosResponse = await this.postRequest( this.apiUrl + `/v1/batch/scrape`, @@ -657,8 +661,10 @@ export default class FirecrawlApp { urls: string[], params?: ScrapeParams, idempotencyKey?: string, + webhook?: CrawlParams["webhook"], + ignoreInvalidURLs?: boolean, ) { - const crawl = await this.asyncBatchScrapeUrls(urls, params, idempotencyKey); + const crawl = await this.asyncBatchScrapeUrls(urls, params, idempotencyKey, webhook, ignoreInvalidURLs); if (crawl.success && crawl.id) { const id = crawl.id; From ccbae4b15568dfa02aa6745f97c95e44a68ade5c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 14 Dec 2024 00:20:14 -0300 Subject: [PATCH 34/52] Update auth.ts --- apps/api/src/controllers/auth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index f865984a..d344625d 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -351,6 +351,7 @@ function getPlanByPriceId(price_id: string | null): PlanType { case process.env.STRIPE_PRICE_ID_ETIER1A_MONTHLY: //ocqh return "etier1a"; case process.env.STRIPE_PRICE_ID_ETIER_SCALE_1_MONTHLY: + case process.env.STRIPE_PRICE_ID_ETIER_SCALE_1_YEARLY: return "etierscale1"; default: return "free"; From c325c3aa337a1f5a6a1924f168233e1565594eda Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 14 Dec 2024 14:55:40 -0300 Subject: [PATCH 35/52] Nick: node sdk patch --- apps/js-sdk/firecrawl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/js-sdk/firecrawl/package.json b/apps/js-sdk/firecrawl/package.json index 30277cc3..74dfcb02 100644 --- a/apps/js-sdk/firecrawl/package.json +++ b/apps/js-sdk/firecrawl/package.json @@ -1,6 +1,6 @@ { "name": "@mendable/firecrawl-js", - "version": "1.9.3", + "version": "1.9.4", "description": "JavaScript SDK for Firecrawl API", "main": "dist/index.js", "types": "dist/index.d.ts", From 664ba69f08e441e0ee109f51ef7a7a3c57b83c23 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 14 Dec 2024 21:40:46 -0300 Subject: [PATCH 36/52] Nick: f-eng monitoring test --- .../controllers/v0/admin/check-fire-engine.ts | 62 +++++++++++++++++++ apps/api/src/routes/admin.ts | 6 ++ 2 files changed, 68 insertions(+) create mode 100644 apps/api/src/controllers/v0/admin/check-fire-engine.ts diff --git a/apps/api/src/controllers/v0/admin/check-fire-engine.ts b/apps/api/src/controllers/v0/admin/check-fire-engine.ts new file mode 100644 index 00000000..8e69d106 --- /dev/null +++ b/apps/api/src/controllers/v0/admin/check-fire-engine.ts @@ -0,0 +1,62 @@ +import { logger } from "../../../lib/logger"; +import * as Sentry from "@sentry/node"; +import { Request, Response } from "express"; + + +export async function checkFireEngine(req: Request, res: Response) { + try { + if (!process.env.FIRE_ENGINE_BETA_URL) { + logger.warn("Fire engine beta URL not configured"); + return res.status(500).json({ + success: false, + error: "Fire engine beta URL not configured", + }); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + + try { + const response = await fetch(`${process.env.FIRE_ENGINE_BETA_URL}/scrape`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Disable-Cache": "true", + }, + body: JSON.stringify({ + url: "https://example.com", + }), + signal: controller.signal, + }); + + clearTimeout(timeout); + + if (response.ok) { + const responseData = await response.json(); + return res.status(200).json({ + data: responseData, + }); + } else { + return res.status(response.status).json({ + success: false, + error: `Fire engine returned status ${response.status}`, + }); + } + } catch (error) { + if (error.name === 'AbortError') { + return res.status(504).json({ + success: false, + error: "Request timed out after 30 seconds", + }); + } + throw error; + } + } catch (error) { + logger.error(error); + Sentry.captureException(error); + return res.status(500).json({ + success: false, + error: "Internal server error", + }); + } +} diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index ec9967b8..1901c6f2 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -8,6 +8,7 @@ import { } from "../controllers/v0/admin/queue"; import { wrap } from "./v1"; import { acucCacheClearController } from "../controllers/v0/admin/acuc-cache-clear"; +import { checkFireEngine } from "../controllers/v0/admin/check-fire-engine"; export const adminRouter = express.Router(); @@ -37,3 +38,8 @@ adminRouter.post( `/admin/${process.env.BULL_AUTH_KEY}/acuc-cache-clear`, wrap(acucCacheClearController), ); + +adminRouter.get( + `/admin/${process.env.BULL_AUTH_KEY}/feng-check`, + wrap(checkFireEngine), +); From 4987880b32f5841b4a2cfb05753b0cc99f4d9f03 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 15 Dec 2024 02:52:06 -0300 Subject: [PATCH 37/52] Nick: random fixes --- .../src/scraper/WebScraper/utils/blocklist.ts | 26 +++++++++++++++- apps/api/src/services/queue-worker.ts | 31 +++++++++---------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/api/src/scraper/WebScraper/utils/blocklist.ts b/apps/api/src/scraper/WebScraper/utils/blocklist.ts index 0a3ef705..ba382040 100644 --- a/apps/api/src/scraper/WebScraper/utils/blocklist.ts +++ b/apps/api/src/scraper/WebScraper/utils/blocklist.ts @@ -6,6 +6,15 @@ configDotenv(); const hashKey = Buffer.from(process.env.HASH_KEY || "", "utf-8"); const algorithm = "aes-256-ecb"; +function encryptAES(plaintext: string, key: Buffer): string { + const cipher = crypto.createCipheriv(algorithm, key, null); + const encrypted = Buffer.concat([ + cipher.update(plaintext, "utf-8"), + cipher.final() + ]); + return encrypted.toString("base64"); +} + function decryptAES(ciphertext: string, key: Buffer): string { const decipher = crypto.createDecipheriv(algorithm, key, null); const decrypted = Buffer.concat([ @@ -42,6 +51,21 @@ const urlBlocklist = [ "PTbGg8PK/h0Seyw4HEpK4Q==", "lZdQMknjHb7+4+sjF3qNTw==", "LsgSq54q5oDysbva29JxnQ==", + "KZfBtpwjOpdSoqacRbz7og==", + "Indtl4yxJMHCKBGF4KABCQ==", + "e3HFXLVgxhaVoadYpwb2BA==", + "b+asgLayXQ5Jq+se+q56jA==", + "86ZDUI7vmp4MvNq3fvZrGQ==", + "sEGFoYZ6GEg4Zocd+TiyfQ==", + "6OOL72eXthgnJ1Hj4PfOQQ==", + "g/ME+Sh1CAFboKrwkVb+5Q==", + "Pw+xawUoX8xBYbX2yqqGWQ==", + "k6vBalxYFhAvkPsF19t9gQ==", + "e3HFXLVgxhaVoadYpwb2BA==", + "b+asgLayXQ5Jq+se+q56jA==", + "KKttwRz4w+AMJrZcB828WQ==", + "vMdzZ33BXoyWVZnAPOBcrg==", + "l8GDVI8w/ueHnNzdN1ODuQ==", ]; const decryptedBlocklist = hashKey.length > 0 ? urlBlocklist.map((ciphertext) => decryptAES(ciphertext, hashKey)) : []; @@ -104,4 +128,4 @@ export function isUrlBlocked(url: string): boolean { logger.error(`Error parsing the following URL: ${url}`); return false; } -} +} \ No newline at end of file diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index 29f4b84f..9fd8861b 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -391,22 +391,21 @@ async function processJob(job: Job & { id: string }, token: string) { // Check if the job URL is researchhub and block it immediately // TODO: remove this once solve the root issue - if ( - job.data.url && - (job.data.url.includes("researchhub.com") || - job.data.url.includes("ebay.com") || - job.data.url.includes("youtube.com")) - ) { - logger.info(`🐂 Blocking job ${job.id} with URL ${job.data.url}`); - const data = { - success: false, - document: null, - project_id: job.data.project_id, - error: - "URL is blocked. Suspecious activity detected. Please contact help@firecrawl.com if you believe this is an error.", - }; - return data; - } + // if ( + // job.data.url && + // (job.data.url.includes("researchhub.com") || + // job.data.url.includes("ebay.com")) + // ) { + // logger.info(`🐂 Blocking job ${job.id} with URL ${job.data.url}`); + // const data = { + // success: false, + // document: null, + // project_id: job.data.project_id, + // error: + // "URL is blocked. Suspecious activity detected. Please contact help@firecrawl.com if you believe this is an error.", + // }; + // return data; + // } try { job.updateProgress({ From 588f747ee87e2fcdc0ddf05bc483bca3d9a7451a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 15 Dec 2024 02:54:49 -0300 Subject: [PATCH 38/52] chore: formatting --- .../controllers/v0/admin/check-fire-engine.ts | 26 ++++--- apps/api/src/main/runWebScraper.ts | 75 +++++++++++-------- .../src/scraper/WebScraper/utils/blocklist.ts | 9 ++- 3 files changed, 64 insertions(+), 46 deletions(-) diff --git a/apps/api/src/controllers/v0/admin/check-fire-engine.ts b/apps/api/src/controllers/v0/admin/check-fire-engine.ts index 8e69d106..0671f7a9 100644 --- a/apps/api/src/controllers/v0/admin/check-fire-engine.ts +++ b/apps/api/src/controllers/v0/admin/check-fire-engine.ts @@ -2,7 +2,6 @@ import { logger } from "../../../lib/logger"; import * as Sentry from "@sentry/node"; import { Request, Response } from "express"; - export async function checkFireEngine(req: Request, res: Response) { try { if (!process.env.FIRE_ENGINE_BETA_URL) { @@ -17,17 +16,20 @@ export async function checkFireEngine(req: Request, res: Response) { const timeout = setTimeout(() => controller.abort(), 30000); try { - const response = await fetch(`${process.env.FIRE_ENGINE_BETA_URL}/scrape`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Disable-Cache": "true", + const response = await fetch( + `${process.env.FIRE_ENGINE_BETA_URL}/scrape`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Disable-Cache": "true", + }, + body: JSON.stringify({ + url: "https://example.com", + }), + signal: controller.signal, }, - body: JSON.stringify({ - url: "https://example.com", - }), - signal: controller.signal, - }); + ); clearTimeout(timeout); @@ -43,7 +45,7 @@ export async function checkFireEngine(req: Request, res: Response) { }); } } catch (error) { - if (error.name === 'AbortError') { + if (error.name === "AbortError") { return res.status(504).json({ success: false, error: "Request timed out after 30 seconds", diff --git a/apps/api/src/main/runWebScraper.ts b/apps/api/src/main/runWebScraper.ts index 411acfe6..63063576 100644 --- a/apps/api/src/main/runWebScraper.ts +++ b/apps/api/src/main/runWebScraper.ts @@ -74,7 +74,16 @@ export async function runWebScraper({ for (let i = 0; i < tries; i++) { if (i > 0) { - logger.debug("Retrying scrape...", { scrapeId: bull_job_id, jobId: bull_job_id, method: "runWebScraper", module: "runWebScraper", tries, i, previousStatusCode: (response as any)?.document?.metadata?.statusCode, previousError: error }); + logger.debug("Retrying scrape...", { + scrapeId: bull_job_id, + jobId: bull_job_id, + method: "runWebScraper", + module: "runWebScraper", + tries, + i, + previousStatusCode: (response as any)?.document?.metadata?.statusCode, + previousError: error, + }); } response = undefined; @@ -100,13 +109,17 @@ export async function runWebScraper({ ); } } - + // This is where the returnvalue from the job is set // onSuccess(response.document, mode); - + engines = response.engines; - if ((response.document.metadata.statusCode >= 200 && response.document.metadata.statusCode < 300) || response.document.metadata.statusCode === 304) { + if ( + (response.document.metadata.statusCode >= 200 && + response.document.metadata.statusCode < 300) || + response.document.metadata.statusCode === 304 + ) { // status code is good -- do not attempt retry break; } @@ -121,34 +134,34 @@ export async function runWebScraper({ } const engineOrder = Object.entries(engines) - .sort((a, b) => a[1].startedAt - b[1].startedAt) - .map((x) => x[0]) as Engine[]; + .sort((a, b) => a[1].startedAt - b[1].startedAt) + .map((x) => x[0]) as Engine[]; - for (const engine of engineOrder) { - const result = engines[engine] as Exclude< - EngineResultsTracker[Engine], - undefined - >; - ScrapeEvents.insert(bull_job_id, { - type: "scrape", - url, - method: engine, - result: { - success: result.state === "success", - response_code: - result.state === "success" ? result.result.statusCode : undefined, - response_size: - result.state === "success" ? result.result.html.length : undefined, - error: - result.state === "error" - ? result.error - : result.state === "timeout" - ? "Timed out" - : undefined, - time_taken: result.finishedAt - result.startedAt, - }, - }); - } + for (const engine of engineOrder) { + const result = engines[engine] as Exclude< + EngineResultsTracker[Engine], + undefined + >; + ScrapeEvents.insert(bull_job_id, { + type: "scrape", + url, + method: engine, + result: { + success: result.state === "success", + response_code: + result.state === "success" ? result.result.statusCode : undefined, + response_size: + result.state === "success" ? result.result.html.length : undefined, + error: + result.state === "error" + ? result.error + : result.state === "timeout" + ? "Timed out" + : undefined, + time_taken: result.finishedAt - result.startedAt, + }, + }); + } if (error === undefined && response?.success) { if (is_scrape === false) { diff --git a/apps/api/src/scraper/WebScraper/utils/blocklist.ts b/apps/api/src/scraper/WebScraper/utils/blocklist.ts index ba382040..16e9e45f 100644 --- a/apps/api/src/scraper/WebScraper/utils/blocklist.ts +++ b/apps/api/src/scraper/WebScraper/utils/blocklist.ts @@ -10,7 +10,7 @@ function encryptAES(plaintext: string, key: Buffer): string { const cipher = crypto.createCipheriv(algorithm, key, null); const encrypted = Buffer.concat([ cipher.update(plaintext, "utf-8"), - cipher.final() + cipher.final(), ]); return encrypted.toString("base64"); } @@ -68,7 +68,10 @@ const urlBlocklist = [ "l8GDVI8w/ueHnNzdN1ODuQ==", ]; -const decryptedBlocklist = hashKey.length > 0 ? urlBlocklist.map((ciphertext) => decryptAES(ciphertext, hashKey)) : []; +const decryptedBlocklist = + hashKey.length > 0 + ? urlBlocklist.map((ciphertext) => decryptAES(ciphertext, hashKey)) + : []; const allowedKeywords = [ "pulse", @@ -128,4 +131,4 @@ export function isUrlBlocked(url: string): boolean { logger.error(`Error parsing the following URL: ${url}`); return false; } -} \ No newline at end of file +} From 842b522b445d1abcbae2f1649b5968cdce4a2835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Fri, 13 Dec 2024 22:30:57 +0100 Subject: [PATCH 39/52] feat: add scrapeOptions.fastMode --- apps/api/src/controllers/v1/types.ts | 3 ++- apps/api/src/lib/cache.ts | 2 +- apps/api/src/scraper/scrapeURL/index.ts | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/src/controllers/v1/types.ts b/apps/api/src/controllers/v1/types.ts index f7226338..2c054560 100644 --- a/apps/api/src/controllers/v1/types.ts +++ b/apps/api/src/controllers/v1/types.ts @@ -182,6 +182,7 @@ export const scrapeOptions = z .optional(), skipTlsVerification: z.boolean().default(false), removeBase64Images: z.boolean().default(true), + fastMode: z.boolean().default(false), }) .strict(strictMessage); @@ -685,11 +686,11 @@ export function fromLegacyScrapeOptions( } : undefined, mobile: pageOptions.mobile, + fastMode: pageOptions.useFastMode, }), internalOptions: { atsv: pageOptions.atsv, v0DisableJsDom: pageOptions.disableJsDom, - v0UseFastMode: pageOptions.useFastMode, }, // TODO: fallback, fetchPageContent, replaceAllPathsWithAbsolutePaths, includeLinks }; diff --git a/apps/api/src/lib/cache.ts b/apps/api/src/lib/cache.ts index 7dcbf88b..cbab4e05 100644 --- a/apps/api/src/lib/cache.ts +++ b/apps/api/src/lib/cache.ts @@ -21,7 +21,7 @@ export function cacheKey( if ( internalOptions.v0CrawlOnlyUrls || internalOptions.forceEngine || - internalOptions.v0UseFastMode || + scrapeOptions.fastMode || internalOptions.atsv || (scrapeOptions.actions && scrapeOptions.actions.length > 0) ) { diff --git a/apps/api/src/scraper/scrapeURL/index.ts b/apps/api/src/scraper/scrapeURL/index.ts index a3eb6f1e..d3b33418 100644 --- a/apps/api/src/scraper/scrapeURL/index.ts +++ b/apps/api/src/scraper/scrapeURL/index.ts @@ -86,7 +86,7 @@ function buildFeatureFlags( flags.add("skipTlsVerification"); } - if (internalOptions.v0UseFastMode) { + if (options.fastMode) { flags.add("useFastMode"); } @@ -148,7 +148,6 @@ export type InternalOptions = { atsv?: boolean; // anti-bot solver, beta v0CrawlOnlyUrls?: boolean; - v0UseFastMode?: boolean; v0DisableJsDom?: boolean; disableSmartWaitCache?: boolean; // Passed along to fire-engine From 5e267f92ffce404e779db3788656152db7e110a5 Mon Sep 17 00:00:00 2001 From: NBR0KN Date: Sat, 14 Dec 2024 20:36:43 +0100 Subject: [PATCH 40/52] fix: adjust Playwright service response to match API schema expectations --- apps/playwright-service-ts/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/playwright-service-ts/api.ts b/apps/playwright-service-ts/api.ts index 90a4eb87..eacb35ff 100644 --- a/apps/playwright-service-ts/api.ts +++ b/apps/playwright-service-ts/api.ts @@ -196,7 +196,7 @@ app.post('/scrape', async (req: Request, res: Response) => { } } - const pageError = pageStatusCode !== 200 ? getError(pageStatusCode) : false; + const pageError = pageStatusCode !== 200 ? getError(pageStatusCode) : undefined; if (!pageError) { console.log(`✅ Scrape successful!`); @@ -209,7 +209,7 @@ app.post('/scrape', async (req: Request, res: Response) => { res.json({ content: pageContent, pageStatusCode, - pageError + ...(pageError && { pageError }) }); }); From afbd01299af9a56250b3cf56e97fc93b48476cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 15:58:27 +0100 Subject: [PATCH 41/52] fix(scrapeURL/fire-engine): timeouts --- apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index 3fc32835..a5ebb9e9 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -136,7 +136,7 @@ export async function scrapeURLWithFireEngineChromeCDP( priority: meta.internalOptions.priority, geolocation: meta.options.geolocation, mobile: meta.options.mobile, - timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic + timeout: meta.options.timeout === undefined ? 300000 : meta.options.timeout, // TODO: better timeout logic disableSmartWaitCache: meta.internalOptions.disableSmartWaitCache, // TODO: scrollXPaths }; @@ -220,7 +220,7 @@ export async function scrapeURLWithFireEnginePlaywright( wait: meta.options.waitFor, geolocation: meta.options.geolocation, - timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic + timeout: meta.options.timeout === undefined ? 300000 : meta.options.timeout, // TODO: better timeout logic }; let response = await performFireEngineScrape( @@ -279,7 +279,7 @@ export async function scrapeURLWithFireEngineTLSClient( geolocation: meta.options.geolocation, disableJsDom: meta.internalOptions.v0DisableJsDom, - timeout: meta.options.timeout === undefined ? 300000 : undefined, // TODO: better timeout logic + timeout: meta.options.timeout === undefined ? 30000 : meta.options.timeout, // TODO: better timeout logic }; let response = await performFireEngineScrape( From b4a5e1a6e9d022d1b7e2163c5996f75ce6be4c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 16:04:17 +0100 Subject: [PATCH 42/52] fix(scrapeURL/fire-engine): timeout handling --- .../scrapeURL/engines/fire-engine/index.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index a5ebb9e9..2b67c4d6 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -120,6 +120,8 @@ export async function scrapeURLWithFireEngineChromeCDP( // Include specified actions ...(meta.options.actions ?? []), ]; + + const timeout = (meta.options.timeout === undefined ? 300000 : Math.round(meta.options.timeout / 3)); const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestChromeCDP = { @@ -136,7 +138,7 @@ export async function scrapeURLWithFireEngineChromeCDP( priority: meta.internalOptions.priority, geolocation: meta.options.geolocation, mobile: meta.options.mobile, - timeout: meta.options.timeout === undefined ? 300000 : meta.options.timeout, // TODO: better timeout logic + timeout, // TODO: better timeout logic disableSmartWaitCache: meta.internalOptions.disableSmartWaitCache, // TODO: scrollXPaths }; @@ -152,7 +154,7 @@ export async function scrapeURLWithFireEngineChromeCDP( request, }), request, - meta.options.timeout !== undefined ? defaultTimeout + totalWait : Infinity, // TODO: better timeout handling + timeout + totalWait, ); specialtyScrapeCheck( @@ -207,6 +209,8 @@ export async function scrapeURLWithFireEngineChromeCDP( export async function scrapeURLWithFireEnginePlaywright( meta: Meta, ): Promise { + const timeout = meta.options.timeout === undefined ? 300000 : Math.round(meta.options.timeout / 3); + const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestPlaywright = { url: meta.url, @@ -220,7 +224,7 @@ export async function scrapeURLWithFireEnginePlaywright( wait: meta.options.waitFor, geolocation: meta.options.geolocation, - timeout: meta.options.timeout === undefined ? 300000 : meta.options.timeout, // TODO: better timeout logic + timeout, }; let response = await performFireEngineScrape( @@ -229,9 +233,7 @@ export async function scrapeURLWithFireEnginePlaywright( request, }), request, - meta.options.timeout !== undefined - ? defaultTimeout + meta.options.waitFor - : Infinity, // TODO: better timeout handling + timeout + meta.options.waitFor, ); specialtyScrapeCheck( @@ -266,6 +268,8 @@ export async function scrapeURLWithFireEnginePlaywright( export async function scrapeURLWithFireEngineTLSClient( meta: Meta, ): Promise { + const timeout = meta.options.timeout === undefined ? 30000 : Math.round(meta.options.timeout / 3); + const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestTLSClient = { url: meta.url, @@ -279,7 +283,7 @@ export async function scrapeURLWithFireEngineTLSClient( geolocation: meta.options.geolocation, disableJsDom: meta.internalOptions.v0DisableJsDom, - timeout: meta.options.timeout === undefined ? 30000 : meta.options.timeout, // TODO: better timeout logic + timeout, }; let response = await performFireEngineScrape( @@ -288,7 +292,7 @@ export async function scrapeURLWithFireEngineTLSClient( request, }), request, - meta.options.timeout !== undefined ? defaultTimeout : Infinity, // TODO: better timeout handling + timeout, ); specialtyScrapeCheck( From 98f27b0acc19ba0f7e956d862db4f1465114fad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 16:29:09 +0100 Subject: [PATCH 43/52] fix(crawl-redis/addCrawlJobDone): further ensure that completed doesn't go over total --- apps/api/src/lib/crawl-redis.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index 3fcd9f67..0c9e0ff0 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -92,12 +92,16 @@ export async function addCrawlJobDone( if (success) { await redisConnection.rpush("crawl:" + id + ":jobs_done_ordered", job_id); - await redisConnection.expire( - "crawl:" + id + ":jobs_done_ordered", - 24 * 60 * 60, - "NX", - ); + } else { + // in case it's already been pushed, make sure it's removed + await redisConnection.lrem("crawl:" + id + ":jobs_done_ordered", -1, job_id); } + + await redisConnection.expire( + "crawl:" + id + ":jobs_done_ordered", + 24 * 60 * 60, + "NX", + ); } export async function getDoneJobsOrderedLength(id: string): Promise { From a5256827c0e92e48913bb514e5fa439083d25ce2 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 15 Dec 2024 14:36:09 -0300 Subject: [PATCH 44/52] Update index.ts --- apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index 2b67c4d6..b84b1e90 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -51,7 +51,11 @@ async function performFireEngineScrape< }); } - if (Date.now() - startTime > timeout) { + const userParam = request.timeout ?? 0; + // Use 70% of the user-provided timeout as the timeout for fire-engine check status + const fireEngineTimeout = timeout + Math.round(userParam * 0.7); + const fullTimeout = Math.max(fireEngineTimeout, timeout); + if (Date.now() - startTime > fullTimeout) { logger.info( "Fire-engine was unable to scrape the page before timing out.", { errors, timeout }, From 0f3a27bf2760c5df0e7c93143dd0ca72c335734c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 18:58:29 +0100 Subject: [PATCH 45/52] fix(scrapeURL/engines): better timeouts --- .../scraper/scrapeURL/engines/fetch/index.ts | 3 ++- .../scrapeURL/engines/fire-engine/index.ts | 19 ++++++++----------- .../src/scraper/scrapeURL/engines/index.ts | 5 +++-- .../scraper/scrapeURL/engines/pdf/index.ts | 7 +++++-- .../scrapeURL/engines/playwright/index.ts | 5 +++-- .../scrapeURL/engines/scrapingbee/index.ts | 9 +++++---- apps/api/src/scraper/scrapeURL/index.ts | 6 +++++- 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts b/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts index af6f57c0..168d9b8f 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fetch/index.ts @@ -5,8 +5,9 @@ import { specialtyScrapeCheck } from "../utils/specialtyHandler"; export async function scrapeURLWithFetch( meta: Meta, + timeToRun: number | undefined ): Promise { - const timeout = 20000; + const timeout = timeToRun ?? 300000; const response = await Promise.race([ fetch(meta.url, { diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index b84b1e90..ef0b41fc 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -18,8 +18,6 @@ import * as Sentry from "@sentry/node"; import { Action } from "../../../../lib/entities"; import { specialtyScrapeCheck } from "../utils/specialtyHandler"; -export const defaultTimeout = 10000; - // This function does not take `Meta` on purpose. It may not access any // meta values to construct the request -- that must be done by the // `scrapeURLWithFireEngine*` functions. @@ -31,7 +29,7 @@ async function performFireEngineScrape< >( logger: Logger, request: FireEngineScrapeRequestCommon & Engine, - timeout = defaultTimeout, + timeout: number, ): Promise { const scrape = await fireEngineScrape( logger.child({ method: "fireEngineScrape" }), @@ -51,11 +49,7 @@ async function performFireEngineScrape< }); } - const userParam = request.timeout ?? 0; - // Use 70% of the user-provided timeout as the timeout for fire-engine check status - const fireEngineTimeout = timeout + Math.round(userParam * 0.7); - const fullTimeout = Math.max(fireEngineTimeout, timeout); - if (Date.now() - startTime > fullTimeout) { + if (Date.now() - startTime > timeout) { logger.info( "Fire-engine was unable to scrape the page before timing out.", { errors, timeout }, @@ -98,6 +92,7 @@ async function performFireEngineScrape< export async function scrapeURLWithFireEngineChromeCDP( meta: Meta, + timeToRun: number | undefined, ): Promise { const actions: Action[] = [ // Transform waitFor option into an action (unsupported by chrome-cdp) @@ -125,7 +120,7 @@ export async function scrapeURLWithFireEngineChromeCDP( ...(meta.options.actions ?? []), ]; - const timeout = (meta.options.timeout === undefined ? 300000 : Math.round(meta.options.timeout / 3)); + const timeout = timeToRun ?? 300000; const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestChromeCDP = { @@ -212,8 +207,9 @@ export async function scrapeURLWithFireEngineChromeCDP( export async function scrapeURLWithFireEnginePlaywright( meta: Meta, + timeToRun: number | undefined, ): Promise { - const timeout = meta.options.timeout === undefined ? 300000 : Math.round(meta.options.timeout / 3); + const timeout = timeToRun ?? 300000; const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestPlaywright = { @@ -271,8 +267,9 @@ export async function scrapeURLWithFireEnginePlaywright( export async function scrapeURLWithFireEngineTLSClient( meta: Meta, + timeToRun: number | undefined, ): Promise { - const timeout = meta.options.timeout === undefined ? 30000 : Math.round(meta.options.timeout / 3); + const timeout = timeToRun ?? 30000; const request: FireEngineScrapeRequestCommon & FireEngineScrapeRequestTLSClient = { diff --git a/apps/api/src/scraper/scrapeURL/engines/index.ts b/apps/api/src/scraper/scrapeURL/engines/index.ts index 01ac0be9..14f263f3 100644 --- a/apps/api/src/scraper/scrapeURL/engines/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/index.ts @@ -105,7 +105,7 @@ export type EngineScrapeResult = { }; const engineHandlers: { - [E in Engine]: (meta: Meta) => Promise; + [E in Engine]: (meta: Meta, timeToRun: number | undefined) => Promise; } = { cache: scrapeCache, "fire-engine;chrome-cdp": scrapeURLWithFireEngineChromeCDP, @@ -372,6 +372,7 @@ export function buildFallbackList(meta: Meta): { export async function scrapeURLWithEngine( meta: Meta, engine: Engine, + timeToRun: number | undefined ): Promise { const fn = engineHandlers[engine]; const logger = meta.logger.child({ @@ -383,5 +384,5 @@ export async function scrapeURLWithEngine( logger, }; - return await fn(_meta); + return await fn(_meta, timeToRun); } diff --git a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts index 341a4f1a..24d5f002 100644 --- a/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/pdf/index.ts @@ -15,6 +15,7 @@ type PDFProcessorResult = { html: string; markdown?: string }; async function scrapePDFWithLlamaParse( meta: Meta, tempFilePath: string, + timeToRun: number | undefined, ): Promise { meta.logger.debug("Processing PDF document with LlamaIndex", { tempFilePath, @@ -63,8 +64,9 @@ async function scrapePDFWithLlamaParse( // TODO: timeout, retries const startedAt = Date.now(); + const timeout = timeToRun ?? 300000; - while (Date.now() <= startedAt + (meta.options.timeout ?? 300000)) { + while (Date.now() <= startedAt + timeout) { try { const result = await robustFetch({ url: `https://api.cloud.llamaindex.ai/api/parsing/job/${jobId}/result/markdown`, @@ -122,7 +124,7 @@ async function scrapePDFWithParsePDF( }; } -export async function scrapePDF(meta: Meta): Promise { +export async function scrapePDF(meta: Meta, timeToRun: number | undefined): Promise { if (!meta.options.parsePDF) { const file = await fetchFileToBuffer(meta.url); const content = file.buffer.toString("base64"); @@ -148,6 +150,7 @@ export async function scrapePDF(meta: Meta): Promise { }), }, tempFilePath, + timeToRun, ); } catch (error) { if (error instanceof Error && error.message === "LlamaParse timed out") { diff --git a/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts b/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts index c92b1d90..edcd50c0 100644 --- a/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/playwright/index.ts @@ -6,8 +6,9 @@ import { robustFetch } from "../../lib/fetch"; export async function scrapeURLWithPlaywright( meta: Meta, + timeToRun: number | undefined, ): Promise { - const timeout = 20000 + meta.options.waitFor; + const timeout = (timeToRun ?? 300000) + meta.options.waitFor; const response = await Promise.race([ await robustFetch({ @@ -30,7 +31,7 @@ export async function scrapeURLWithPlaywright( }), }), (async () => { - await new Promise((resolve) => setTimeout(() => resolve(null), 20000)); + await new Promise((resolve) => setTimeout(() => resolve(null), timeout)); throw new TimeoutError( "Playwright was unable to scrape the page before timing out", { cause: { timeout } }, diff --git a/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts b/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts index 50ac502b..db702a44 100644 --- a/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/scrapingbee/index.ts @@ -9,16 +9,17 @@ const client = new ScrapingBeeClient(process.env.SCRAPING_BEE_API_KEY!); export function scrapeURLWithScrapingBee( wait_browser: "domcontentloaded" | "networkidle2", -): (meta: Meta) => Promise { - return async (meta: Meta): Promise => { +): (meta: Meta, timeToRun: number | undefined) => Promise { + return async (meta: Meta, timeToRun: number | undefined): Promise => { let response: AxiosResponse; + const timeout = (timeToRun ?? 300000) + meta.options.waitFor; try { response = await client.get({ url: meta.url, params: { - timeout: 15000, // TODO: dynamic timeout based on request timeout + timeout, wait_browser: wait_browser, - wait: Math.min(meta.options.waitFor, 35000), + wait: meta.options.waitFor, transparent_status_code: true, json_response: true, screenshot: meta.options.formats.includes("screenshot"), diff --git a/apps/api/src/scraper/scrapeURL/index.ts b/apps/api/src/scraper/scrapeURL/index.ts index d3b33418..c0b6d4e5 100644 --- a/apps/api/src/scraper/scrapeURL/index.ts +++ b/apps/api/src/scraper/scrapeURL/index.ts @@ -202,11 +202,15 @@ async function scrapeURLLoop(meta: Meta): Promise { const results: EngineResultsTracker = {}; let result: EngineScrapeResultWithContext | null = null; + const timeToRun = meta.options.timeout !== undefined + ? Math.round(meta.options.timeout / Math.min(fallbackList.length, 3)) + : undefined + for (const { engine, unsupportedFeatures } of fallbackList) { const startedAt = Date.now(); try { meta.logger.info("Scraping via " + engine + "..."); - const _engineResult = await scrapeURLWithEngine(meta, engine); + const _engineResult = await scrapeURLWithEngine(meta, engine, timeToRun); if (_engineResult.markdown === undefined) { // Some engines emit Markdown directly. _engineResult.markdown = await parseMarkdown(_engineResult.html); From 1214d219e12cdec4fff6257b0ef2a5c873818f17 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 15 Dec 2024 15:43:12 -0300 Subject: [PATCH 46/52] Nick: fix actions errors --- apps/api/src/main/runWebScraper.ts | 1 + .../scrapeURL/engines/fire-engine/checkStatus.ts | 8 +++++++- .../src/scraper/scrapeURL/engines/fire-engine/index.ts | 8 ++++++-- apps/api/src/scraper/scrapeURL/error.ts | 10 ++++++++++ apps/api/src/scraper/scrapeURL/index.ts | 5 +++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/api/src/main/runWebScraper.ts b/apps/api/src/main/runWebScraper.ts index 63063576..83e899bb 100644 --- a/apps/api/src/main/runWebScraper.ts +++ b/apps/api/src/main/runWebScraper.ts @@ -96,6 +96,7 @@ export async function runWebScraper({ ...internalOptions, }); if (!response.success) { + error = response.error; if (response.error instanceof Error) { throw response.error; } else { diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts index 328931ba..6f65db98 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/checkStatus.ts @@ -3,7 +3,7 @@ import * as Sentry from "@sentry/node"; import { z } from "zod"; import { robustFetch } from "../../lib/fetch"; -import { EngineError, SiteError } from "../../error"; +import { ActionError, EngineError, SiteError } from "../../error"; const successSchema = z.object({ jobId: z.string(), @@ -111,6 +111,12 @@ export async function fireEngineCheckStatus( status.error.includes("Chrome error: ") ) { throw new SiteError(status.error.split("Chrome error: ")[1]); + } else if ( + typeof status.error === "string" && + // TODO: improve this later + status.error.includes("Element") + ) { + throw new ActionError(status.error.split("Error: ")[1]); } else { throw new EngineError("Scrape job failed", { cause: { diff --git a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts index ef0b41fc..a2deeed2 100644 --- a/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts +++ b/apps/api/src/scraper/scrapeURL/engines/fire-engine/index.ts @@ -13,7 +13,7 @@ import { FireEngineCheckStatusSuccess, StillProcessingError, } from "./checkStatus"; -import { EngineError, SiteError, TimeoutError } from "../../error"; +import { ActionError, EngineError, SiteError, TimeoutError } from "../../error"; import * as Sentry from "@sentry/node"; import { Action } from "../../../../lib/entities"; import { specialtyScrapeCheck } from "../utils/specialtyHandler"; @@ -68,7 +68,11 @@ async function performFireEngineScrape< } catch (error) { if (error instanceof StillProcessingError) { // nop - } else if (error instanceof EngineError || error instanceof SiteError) { + } else if ( + error instanceof EngineError || + error instanceof SiteError || + error instanceof ActionError + ) { logger.debug("Fire-engine scrape job failed.", { error, jobId: scrape.jobId, diff --git a/apps/api/src/scraper/scrapeURL/error.ts b/apps/api/src/scraper/scrapeURL/error.ts index ec044745..0a4f6e5b 100644 --- a/apps/api/src/scraper/scrapeURL/error.ts +++ b/apps/api/src/scraper/scrapeURL/error.ts @@ -56,3 +56,13 @@ export class SiteError extends Error { this.code = code; } } + +export class ActionError extends Error { + public code: string; + constructor(code: string) { + super( + "Action(s) failed to complete. Error code: " + code, + ); + this.code = code; + } +} diff --git a/apps/api/src/scraper/scrapeURL/index.ts b/apps/api/src/scraper/scrapeURL/index.ts index c0b6d4e5..800457a8 100644 --- a/apps/api/src/scraper/scrapeURL/index.ts +++ b/apps/api/src/scraper/scrapeURL/index.ts @@ -12,6 +12,7 @@ import { } from "./engines"; import { parseMarkdown } from "../../lib/html-to-markdown"; import { + ActionError, AddFeatureError, EngineError, NoEnginesLeftError, @@ -288,6 +289,8 @@ async function scrapeURLLoop(meta: Meta): Promise { throw error; } else if (error instanceof SiteError) { throw error; + } else if (error instanceof ActionError) { + throw error; } else { Sentry.captureException(error); meta.logger.info( @@ -408,6 +411,8 @@ export async function scrapeURL( // TODO: results? } else if (error instanceof SiteError) { meta.logger.warn("scrapeURL: Site failed to load in browser", { error }); + } else if (error instanceof ActionError) { + meta.logger.warn("scrapeURL: Action(s) failed to complete", { error }); } else { Sentry.captureException(error); meta.logger.error("scrapeURL: Unexpected error happened", { error }); From 126b46ee2c16cc7d734d75bc73c090444c29afd1 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 15 Dec 2024 15:53:24 -0300 Subject: [PATCH 47/52] Update issue_credits.ts --- apps/api/src/services/billing/issue_credits.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/services/billing/issue_credits.ts b/apps/api/src/services/billing/issue_credits.ts index ce84db1b..2ca057dd 100644 --- a/apps/api/src/services/billing/issue_credits.ts +++ b/apps/api/src/services/billing/issue_credits.ts @@ -9,6 +9,7 @@ export async function issueCredits(team_id: string, credits: number) { status: "active", // indicates that this coupon was issued from auto recharge from_auto_recharge: true, + initial_credits: credits, }); if (error) { From 30fa78cd9e950899a2359122048e7231edd6bf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 20:16:29 +0100 Subject: [PATCH 48/52] feat(queue-worker): fix redirect slipping --- apps/api/src/services/queue-worker.ts | 40 ++++++++++++------------- apps/api/{ => utils}/logview.js | 0 apps/api/utils/urldump-redis.js | 14 +++++++++ apps/api/utils/urldump.js | 43 +++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 21 deletions(-) rename apps/api/{ => utils}/logview.js (100%) create mode 100644 apps/api/utils/urldump-redis.js create mode 100644 apps/api/utils/urldump.js diff --git a/apps/api/src/services/queue-worker.ts b/apps/api/src/services/queue-worker.ts index 9fd8861b..c2d2e2c6 100644 --- a/apps/api/src/services/queue-worker.ts +++ b/apps/api/src/services/queue-worker.ts @@ -481,33 +481,30 @@ async function processJob(job: Job & { id: string }, token: string) { normalizeURL(doc.metadata.url, sc) !== normalizeURL(doc.metadata.sourceURL, sc) ) { - logger.debug( - "Was redirected, removing old URL and locking new URL...", - { oldUrl: doc.metadata.sourceURL, newUrl: doc.metadata.url }, - ); - // Remove the old URL from visited unique due to checking for limit - // Do not remove from :visited otherwise it will keep crawling the original URL (sourceURL) - await redisConnection.srem( - "crawl:" + job.data.crawl_id + ":visited_unique", - normalizeURL(doc.metadata.sourceURL, sc), - ); - const p1 = generateURLPermutations(normalizeURL(doc.metadata.url, sc)); const p2 = generateURLPermutations( normalizeURL(doc.metadata.sourceURL, sc), ); - // In crawls, we should only crawl a redirected page once, no matter how many; times it is redirected to, or if it's been discovered by the crawler before. - // This can prevent flakiness with race conditions. - // Lock the new URL - const lockRes = await lockURL(job.data.crawl_id, sc, doc.metadata.url); - if ( - job.data.crawlerOptions !== null && - !lockRes && - JSON.stringify(p1) !== JSON.stringify(p2) - ) { - throw new RacedRedirectError(); + if (JSON.stringify(p1) !== JSON.stringify(p2)) { + logger.debug( + "Was redirected, removing old URL and locking new URL...", + { oldUrl: doc.metadata.sourceURL, newUrl: doc.metadata.url }, + ); + + // Prevent redirect target from being visited in the crawl again + // See lockURL + const x = await redisConnection.sadd( + "crawl:" + job.data.crawl_id + ":visited", + ...p1.map(x => x.href), + ); + const lockRes = x === p1.length; + + if (job.data.crawlerOptions !== null && !lockRes) { + throw new RacedRedirectError(); + } } + } logger.debug("Logging job to DB..."); @@ -678,6 +675,7 @@ async function processJob(job: Job & { id: string }, token: string) { logger.debug("Declaring job as done..."); await addCrawlJobDone(job.data.crawl_id, job.id, false); + await redisConnection.srem("crawl:" + job.data.crawl_id + ":visited_unique", normalizeURL(job.data.url, sc)); logger.debug("Logging job to DB..."); await logJob( diff --git a/apps/api/logview.js b/apps/api/utils/logview.js similarity index 100% rename from apps/api/logview.js rename to apps/api/utils/logview.js diff --git a/apps/api/utils/urldump-redis.js b/apps/api/utils/urldump-redis.js new file mode 100644 index 00000000..fdd6090c --- /dev/null +++ b/apps/api/utils/urldump-redis.js @@ -0,0 +1,14 @@ +require("dotenv").config(); +const Redis = require("ioredis"); + +const crawlId = process.argv[2]; + +const redisConnection = new Redis(process.env.REDIS_URL, { + maxRetriesPerRequest: null, +}); + +(async () => { + const res = await redisConnection.sscan("crawl:" + crawlId + ":visited_unique", 0, "COUNT", 999); + await require("fs/promises").writeFile(crawlId + "-visited.txt", res[1].map(x => x.split("://").slice(1).join("://")).sort().join("\n")); + process.exit(0); +})(); \ No newline at end of file diff --git a/apps/api/utils/urldump.js b/apps/api/utils/urldump.js new file mode 100644 index 00000000..3583f7c6 --- /dev/null +++ b/apps/api/utils/urldump.js @@ -0,0 +1,43 @@ +require("dotenv").config(); + +//const baseUrl = "https://api.firecrawl.dev"; +const baseUrl = "http://localhost:3002"; +const crawlId = process.argv[2]; + +(async () => { + let url = baseUrl + "/v1/crawl/" + crawlId; + let urls = []; + + while (url) { + let res; + + while (true) { + try { + res = (await (await fetch(url, { + headers: { + "Authorization": "Bearer " + process.env.TEST_API_KEY + } + })).json()); + break; + } catch (e) { + console.error(e); + } + } + + console.log(res.data.length); + if (res.data.length === 0) { + break; + } + + urls.push(...res.data.map(x => x.metadata.url ?? x.metadata.sourceURL)); + + url = res.next; + if (url !== undefined) { + const o = new URL(url) + o.protocol = new URL(baseUrl).protocol; + url = o.href; + } + } + + await require("fs/promises").writeFile(crawlId + "-urls.txt", urls.map(x => x.split("://").slice(1).join("://")).sort().join("\n")); +})(); \ No newline at end of file From 37f58efe457dd985e3c01aabeed23d48cf3bc99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 21:01:31 +0100 Subject: [PATCH 49/52] fix(crawl-redis/lockURL): only add to visited_unique if lock succeeds --- apps/api/src/lib/crawl-redis.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/api/src/lib/crawl-redis.ts b/apps/api/src/lib/crawl-redis.ts index 0c9e0ff0..602d13b3 100644 --- a/apps/api/src/lib/crawl-redis.ts +++ b/apps/api/src/lib/crawl-redis.ts @@ -233,13 +233,6 @@ export async function lockURL( url = normalizeURL(url, sc); logger = logger.child({ url }); - await redisConnection.sadd("crawl:" + id + ":visited_unique", url); - await redisConnection.expire( - "crawl:" + id + ":visited_unique", - 24 * 60 * 60, - "NX", - ); - let res: boolean; if (!sc.crawlerOptions?.deduplicateSimilarURLs) { res = (await redisConnection.sadd("crawl:" + id + ":visited", url)) !== 0; @@ -255,6 +248,15 @@ export async function lockURL( await redisConnection.expire("crawl:" + id + ":visited", 24 * 60 * 60, "NX"); + if (res) { + await redisConnection.sadd("crawl:" + id + ":visited_unique", url); + await redisConnection.expire( + "crawl:" + id + ":visited_unique", + 24 * 60 * 60, + "NX", + ); + } + logger.debug("Locking URL " + JSON.stringify(url) + "... result: " + res, { res, }); From e97ee4a4be9b856707cae547eedd6d9f015d94e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 22:33:36 +0100 Subject: [PATCH 50/52] fix(WebScraper/tryGetSitemap): deduplicate sitemap links list --- apps/api/src/scraper/WebScraper/crawler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/scraper/WebScraper/crawler.ts b/apps/api/src/scraper/WebScraper/crawler.ts index 19b0b5b4..2e47d352 100644 --- a/apps/api/src/scraper/WebScraper/crawler.ts +++ b/apps/api/src/scraper/WebScraper/crawler.ts @@ -210,7 +210,7 @@ export class WebCrawler { } if (sitemapLinks.length > 0) { let filteredLinks = this.filterLinks( - sitemapLinks, + [...new Set(sitemapLinks)], this.limit, this.maxCrawledDepth, fromMap, From 72d6a8179e35eaeff4b6db159c6320bdce2e22f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 23:08:23 +0100 Subject: [PATCH 51/52] fix(rate-limiter): raise crawlStatus limits --- apps/api/src/services/rate-limiter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/rate-limiter.ts b/apps/api/src/services/rate-limiter.ts index 5b8e39ca..21025589 100644 --- a/apps/api/src/services/rate-limiter.ts +++ b/apps/api/src/services/rate-limiter.ts @@ -80,8 +80,8 @@ const RATE_LIMITS = { default: 100, }, crawlStatus: { - free: 300, - default: 500, + free: 500, + default: 5000, }, testSuite: { free: 10000, From 2de659d81050903014709f22a6cc7170625e47c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20M=C3=B3ricz?= Date: Sun, 15 Dec 2024 23:54:52 +0100 Subject: [PATCH 52/52] fix(queue-jobs): fix concurrency limit --- apps/api/src/controllers/v1/crawl.ts | 8 +- apps/api/src/services/queue-jobs.ts | 150 ++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 32 deletions(-) diff --git a/apps/api/src/controllers/v1/crawl.ts b/apps/api/src/controllers/v1/crawl.ts index 1fb470f9..c2e3369f 100644 --- a/apps/api/src/controllers/v1/crawl.ts +++ b/apps/api/src/controllers/v1/crawl.ts @@ -18,7 +18,7 @@ import { } from "../../lib/crawl-redis"; import { logCrawl } from "../../services/logging/crawl_log"; import { getScrapeQueue } from "../../services/queue-service"; -import { addScrapeJob } from "../../services/queue-jobs"; +import { addScrapeJob, addScrapeJobs } from "../../services/queue-jobs"; import { logger as _logger } from "../../lib/logger"; import { getJobPriority } from "../../lib/job-priority"; import { callWebhook } from "../../services/webhook"; @@ -139,9 +139,9 @@ export async function crawlController( name: uuid, data: { url, - mode: "single_urls", + mode: "single_urls" as const, team_id: req.auth.team_id, - plan: req.auth.plan, + plan: req.auth.plan!, crawlerOptions, scrapeOptions, internalOptions: sc.internalOptions, @@ -170,7 +170,7 @@ export async function crawlController( jobs.map((x) => x.opts.jobId), ); logger.debug("Adding scrape jobs to BullMQ..."); - await getScrapeQueue().addBulk(jobs); + await addScrapeJobs(jobs); } else { logger.debug("Sitemap not found or ignored.", { ignoreSitemap: sc.crawlerOptions.ignoreSitemap, diff --git a/apps/api/src/services/queue-jobs.ts b/apps/api/src/services/queue-jobs.ts index ee9e6177..6ce48a81 100644 --- a/apps/api/src/services/queue-jobs.ts +++ b/apps/api/src/services/queue-jobs.ts @@ -11,11 +11,50 @@ import { pushConcurrencyLimitedJob, } from "../lib/concurrency-limit"; +async function _addScrapeJobToConcurrencyQueue( + webScraperOptions: any, + options: any, + jobId: string, + jobPriority: number, +) { + await pushConcurrencyLimitedJob(webScraperOptions.team_id, { + id: jobId, + data: webScraperOptions, + opts: { + ...options, + priority: jobPriority, + jobId: jobId, + }, + priority: jobPriority, + }); +} + +async function _addScrapeJobToBullMQ( + webScraperOptions: any, + options: any, + jobId: string, + jobPriority: number, +) { + if ( + webScraperOptions && + webScraperOptions.team_id && + webScraperOptions.plan + ) { + await pushConcurrencyLimitActiveJob(webScraperOptions.team_id, jobId); + } + + await getScrapeQueue().add(jobId, webScraperOptions, { + ...options, + priority: jobPriority, + jobId, + }); +} + async function addScrapeJobRaw( webScraperOptions: any, options: any, jobId: string, - jobPriority: number = 10, + jobPriority: number, ) { let concurrencyLimited = false; @@ -33,30 +72,9 @@ async function addScrapeJobRaw( } if (concurrencyLimited) { - await pushConcurrencyLimitedJob(webScraperOptions.team_id, { - id: jobId, - data: webScraperOptions, - opts: { - ...options, - priority: jobPriority, - jobId: jobId, - }, - priority: jobPriority, - }); + await _addScrapeJobToConcurrencyQueue(webScraperOptions, options, jobId, jobPriority); } else { - if ( - webScraperOptions && - webScraperOptions.team_id && - webScraperOptions.plan - ) { - await pushConcurrencyLimitActiveJob(webScraperOptions.team_id, jobId); - } - - await getScrapeQueue().add(jobId, webScraperOptions, { - ...options, - priority: jobPriority, - jobId, - }); + await _addScrapeJobToBullMQ(webScraperOptions, options, jobId, jobPriority); } } @@ -109,11 +127,87 @@ export async function addScrapeJobs( }[], ) { if (jobs.length === 0) return true; - // TODO: better + + let countCanBeDirectlyAdded = Infinity; + + if ( + jobs[0].data && + jobs[0].data.team_id && + jobs[0].data.plan + ) { + const now = Date.now(); + const limit = await getConcurrencyLimitMax(jobs[0].data.plan); + console.log("CC limit", limit); + cleanOldConcurrencyLimitEntries(jobs[0].data.team_id, now); + + countCanBeDirectlyAdded = Math.max(limit - (await getConcurrencyLimitActiveJobs(jobs[0].data.team_id, now)).length, 0); + } + + const addToBull = jobs.slice(0, countCanBeDirectlyAdded); + const addToCQ = jobs.slice(countCanBeDirectlyAdded); + await Promise.all( - jobs.map((job) => - addScrapeJob(job.data, job.opts, job.opts.jobId, job.opts.priority), - ), + addToBull.map(async (job) => { + const size = JSON.stringify(job.data).length; + return await Sentry.startSpan( + { + name: "Add scrape job", + op: "queue.publish", + attributes: { + "messaging.message.id": job.opts.jobId, + "messaging.destination.name": getScrapeQueue().name, + "messaging.message.body.size": size, + }, + }, + async (span) => { + await _addScrapeJobToBullMQ( + { + ...job.data, + sentry: { + trace: Sentry.spanToTraceHeader(span), + baggage: Sentry.spanToBaggageHeader(span), + size, + }, + }, + job.opts, + job.opts.jobId, + job.opts.priority, + ); + }, + ); + }), + ); + + await Promise.all( + addToCQ.map(async (job) => { + const size = JSON.stringify(job.data).length; + return await Sentry.startSpan( + { + name: "Add scrape job", + op: "queue.publish", + attributes: { + "messaging.message.id": job.opts.jobId, + "messaging.destination.name": getScrapeQueue().name, + "messaging.message.body.size": size, + }, + }, + async (span) => { + await _addScrapeJobToConcurrencyQueue( + { + ...job.data, + sentry: { + trace: Sentry.spanToTraceHeader(span), + baggage: Sentry.spanToBaggageHeader(span), + size, + }, + }, + job.opts, + job.opts.jobId, + job.opts.priority, + ); + }, + ); + }), ); }

You are approaching your credit limit for this billing period. Your usage right now is around 80% of your total credit limit. Consider upgrading your plan to avoid hitting the limit. Check out our pricing page for more info.