fix: filter sitemap-derived URLs by enqueue strategy#3797
Conversation
| export type SearchParams = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>; | ||
|
|
||
| /** Enqueue strategy values, mirroring the `EnqueueStrategy` enum from `@crawlee/core` (which `@crawlee/utils` can't import). */ | ||
| export type EnqueueStrategyValue = 'all' | 'same-hostname' | 'same-domain' | 'same-origin'; |
There was a problem hiding this comment.
same here
| export type EnqueueStrategyValue = 'all' | 'same-hostname' | 'same-domain' | 'same-origin'; | |
| export type EnqueueStrategyValue = `${EnqueueStrategy}`; |
i would rather inline this instead of introducing a new exported type for it
There was a problem hiding this comment.
This is not possible because crawlee/core already depends on crawlee/utils, so that would be a circular reference. Am I correct? 🙂
There was a problem hiding this comment.
I won't pretend to be a guru here, but this still doesn't seem doable (or at least not doable in a right & easy way):
Type-level cycles are fine within a single tsc program (or with project references) — but that's not the situation here. @crawlee/utils and @crawlee/core are two separately-compiled packages, and this repo doesn't use project references: each package is built by its own tsc -p tsconfig.build.json, ordered topologically by Turbo from the package.json dependency graph.
To write
${EnqueueStrategy}in utils I'd have to import type { EnqueueStrategy } from '@crawlee/core', and that hits two concrete walls:
- It requires @crawlee/core in utils' package.json. As soon as I add it, Turbo refuses to build: x Cyclic dependency detected: @crawlee/core#build, @crawlee/utils#build
- Even without declaring it (relying on the workspace symlink), the build fails. tsconfig.build.json has no paths mapping — only the root dev/type-check tsconfig.json does — so cross-package types resolve from the emitted node_modules/@crawlee/core/dist/index.d.ts. That file only exists after core is built, but core builds after utils (core depends on utils), so on a clean build utils compiles first and gets TS2307: Cannot find module '@crawlee/core'.
So I kept the mirrored literal union in utils. I did apply the
${EnqueueStrategy}form in sitemap_request_list.ts, since that's in core where the enum is already in scope. If we ever want to genuinely dedupe it, the value union would have to move into @crawlee/types (which both packages depend on), but that's a bigger change than this nit.
There was a problem hiding this comment.
Could we perhaps extract all the EnqueueStrategy logic to @crawlee/utils (and only import / reexport it in @crawlee/core and all the places we want to use it)?
It imo makes sense now that we're directly using the strategies in @crawlee/utils classes (Sitemap, Robots)
There was a problem hiding this comment.
The reason is probably confirmed by #3797 (comment), so resolving this with no action.
There was a problem hiding this comment.
unresolving, as a new comment from barjin landed ⛵
There was a problem hiding this comment.
Could we perhaps extract all the EnqueueStrategy logic to @crawlee/utils (and only import / reexport it in @crawlee/core and all the places we want to use it)?
It imo makes sense now that we're directly using the strategies in @crawlee/utils classes (Sitemap, Robots)
moving it there sounds reasonable, thanks
|
@B4nan FYI; this is still a draft - I haven't reviewed most of it yet |
|
no worries, i was just curious 🙃 |
|
Requesting a review from @janbuchar, since you may have additional context from the Python solution. |
| networkTimeouts, | ||
| reportNetworkErrors = true, | ||
| nestedSitemapFilter, | ||
| enqueueStrategy = 'same-hostname', |
There was a problem hiding this comment.
[medium] Defaulting to same-hostname makes parseSitemap/RobotsTxtFile.* drop cross-host URLs (notably apex↔www) that they used to return. Intended and consistent with enqueueLinks, but these are public exports — needs a changelog note.
There was a problem hiding this comment.
So I guess we should update the changelog manually once this is released, after the generated section is moved to the next release.
| * Check whether `target` matches `origin` under the given enqueue `strategy`. The URL scheme is not | ||
| * considered here (use {@apilink filterUrl} for the combined scheme + strategy check). | ||
| */ | ||
| export function matchesEnqueueStrategy(strategy: EnqueueStrategyValue, target: URL, origin: URL): boolean { |
There was a problem hiding this comment.
[low] Duplicates the EnqueueStrategy matching in core/enqueue_links.ts (unavoidable — utils can't import core). Add a comment cross-referencing the core enum so they don't drift. Minor: core's same-hostname is port-sensitive, this isn't.
There was a problem hiding this comment.
Let's add a cross-reference note to the docblock.
There was a problem hiding this comment.
If I am not mistaken the comment also confirms this #3797 (comment)
| if (source.type === 'url') { | ||
| const { allowed, reason } = filterUrl(item.loc, sitemapUrl!, enqueueStrategy); | ||
| if (!allowed) { | ||
| log.debug(`Skipping sitemap URL ${item.loc} (parent ${source.url}): ${reason}.`); |
There was a problem hiding this comment.
[low] Dropped <url> entries log at debug while dropped nested sitemaps log at warning (line 373). The <url> case is the more impactful one — align the levels.
There was a problem hiding this comment.
Just flipping it to a warning could generate a lot of warning logs. So let's update the parseSitemap to count the dropped <url> records and emit a single aggregated warning per sitemap instead.
| const originUrl = toUrl(origin); | ||
|
|
||
| if (originUrl === null || !matchesEnqueueStrategy(strategy, targetUrl, originUrl)) { | ||
| return { allowed: false, reason: `does not match enqueue strategy '${strategy}'` }; |
There was a problem hiding this comment.
[low] Reason says does not match enqueue strategy even when the real cause is an unparseable origin. Give the origin-parse-failure path its own reason.
There was a problem hiding this comment.
Let's split it and return a proper reason: "invalid origin URL" instead.
| */ | ||
| getSitemaps(): string[] { | ||
| return this.robots.getSitemaps(); | ||
| getSitemaps(enqueueStrategy: EnqueueStrategyValue = 'same-hostname'): string[] { |
There was a problem hiding this comment.
let's please make these options objects
getSitemaps(options: { enqueueStrategy: EnqueueStrategy })in TS, this is a positional argument, so if we eventually add a new option, we'll have to always specify the enqueueStrategy
getSitemaps('same-hostname', /* <--- this has to be here in order to specify the second argument */)There was a problem hiding this comment.
Sure, thank you
Edit: applied this approach to other places as well
| /** | ||
| * Keep only sitemap-derived URLs matching this strategy relative to the parent sitemap URL; non-`http(s)` | ||
| * schemes are always dropped. The strategy is also stamped onto emitted `Request`s, so it stays enforced | ||
| * after navigation (e.g. across redirects). Pass `'all'` to disable host filtering. | ||
| * @default EnqueueStrategy.SameHostname | ||
| */ | ||
| enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`; |
There was a problem hiding this comment.
The strategy is also stamped onto emitted
Requests, so it stays enforced after navigation (e.g. across redirects).
This is an implementation detail (see that Request.enqueueStrategy is private). I wish we had implemented this differently, but here we are 😄
| export type SearchParams = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>; | ||
|
|
||
| /** Enqueue strategy values, mirroring the `EnqueueStrategy` enum from `@crawlee/core` (which `@crawlee/utils` can't import). */ | ||
| export type EnqueueStrategyValue = 'all' | 'same-hostname' | 'same-domain' | 'same-origin'; |
There was a problem hiding this comment.
Could we perhaps extract all the EnqueueStrategy logic to @crawlee/utils (and only import / reexport it in @crawlee/core and all the places we want to use it)?
It imo makes sense now that we're directly using the strategies in @crawlee/utils classes (Sitemap, Robots)
SitemapRequestList,Sitemap.load/parseSitemap, andRobotsTxtFile.getSitemaps()now apply an enqueue strategy to the URLs they extract. They keep only entries that match the strategy (defaultsame-hostname) relative to their parent sitemap, and always drop non-http(s)schemes. This brings sitemap loading in line withenqueueLinks, which already scopes discovered links to the same hostname by default.The selected strategy is also stamped onto the emitted
Requestobjects, so it keeps being enforced after navigation (e.g. across redirects).This changes the default behavior: cross-host sitemap entries are no longer enqueued unless you opt in with
enqueueStrategy: 'all'(or'same-domain'/'same-origin').