@crawlee/core 4.0.0-beta.12 → 4.0.0-beta.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/README.md +17 -13
  2. package/autoscaling/autoscaled_pool.d.ts +67 -172
  3. package/autoscaling/autoscaled_pool.js +165 -320
  4. package/autoscaling/client_load_signal.d.ts +55 -0
  5. package/autoscaling/client_load_signal.js +73 -0
  6. package/autoscaling/concurrency_system.d.ts +268 -0
  7. package/autoscaling/concurrency_system.js +351 -0
  8. package/autoscaling/cpu_load_signal.d.ts +43 -0
  9. package/autoscaling/cpu_load_signal.js +47 -0
  10. package/autoscaling/event_loop_load_signal.d.ts +51 -0
  11. package/autoscaling/event_loop_load_signal.js +60 -0
  12. package/autoscaling/index.d.ts +6 -1
  13. package/autoscaling/index.js +6 -1
  14. package/autoscaling/load_signal.d.ts +99 -0
  15. package/autoscaling/load_signal.js +104 -0
  16. package/autoscaling/memory_load_signal.d.ts +47 -0
  17. package/autoscaling/memory_load_signal.js +106 -0
  18. package/autoscaling/snapshotter.d.ts +58 -163
  19. package/autoscaling/snapshotter.js +45 -263
  20. package/autoscaling/system_status.d.ts +62 -84
  21. package/autoscaling/system_status.js +92 -122
  22. package/autoscaling/weighted_avg.d.ts +5 -0
  23. package/autoscaling/weighted_avg.js +14 -0
  24. package/byte_utils.d.ts +17 -0
  25. package/byte_utils.js +42 -0
  26. package/configuration.d.ts +96 -223
  27. package/configuration.js +170 -222
  28. package/cookie_utils.d.ts +4 -3
  29. package/cookie_utils.js +22 -13
  30. package/crawlers/context_pipeline.d.ts +10 -1
  31. package/crawlers/context_pipeline.js +31 -8
  32. package/crawlers/crawler_commons.d.ts +90 -83
  33. package/crawlers/crawler_commons.js +1 -116
  34. package/crawlers/error_snapshotter.d.ts +2 -5
  35. package/crawlers/error_snapshotter.js +7 -8
  36. package/crawlers/error_tracker.d.ts +0 -1
  37. package/crawlers/error_tracker.js +0 -1
  38. package/crawlers/index.d.ts +1 -3
  39. package/crawlers/index.js +0 -3
  40. package/crawlers/internals/types.d.ts +0 -1
  41. package/crawlers/internals/types.js +0 -1
  42. package/crawlers/statistics.d.ts +143 -59
  43. package/crawlers/statistics.js +243 -153
  44. package/debug.d.ts +36 -0
  45. package/debug.js +70 -0
  46. package/enqueue_links/enqueue_links.d.ts +59 -68
  47. package/enqueue_links/enqueue_links.js +57 -62
  48. package/enqueue_links/index.d.ts +0 -1
  49. package/enqueue_links/index.js +0 -1
  50. package/enqueue_links/shared.d.ts +40 -27
  51. package/enqueue_links/shared.js +90 -68
  52. package/errors.d.ts +72 -4
  53. package/errors.js +89 -5
  54. package/events/event_manager.d.ts +35 -9
  55. package/events/event_manager.js +10 -12
  56. package/events/index.d.ts +0 -1
  57. package/events/index.js +0 -1
  58. package/events/local_event_manager.d.ts +15 -3
  59. package/events/local_event_manager.js +39 -13
  60. package/http.d.ts +9 -0
  61. package/http.js +28 -0
  62. package/index.d.ts +7 -4
  63. package/index.js +6 -3
  64. package/iterables.d.ts +79 -0
  65. package/iterables.js +134 -0
  66. package/log.d.ts +82 -3
  67. package/log.js +106 -1
  68. package/memory-storage/consts.d.ts +4 -0
  69. package/memory-storage/consts.js +4 -0
  70. package/memory-storage/index.d.ts +1 -0
  71. package/memory-storage/index.js +1 -0
  72. package/memory-storage/memory-storage.d.ts +38 -0
  73. package/memory-storage/memory-storage.js +130 -0
  74. package/memory-storage/resource-clients/common/base-client.d.ts +4 -0
  75. package/memory-storage/resource-clients/common/base-client.js +6 -0
  76. package/memory-storage/resource-clients/dataset.d.ts +40 -0
  77. package/memory-storage/resource-clients/dataset.js +114 -0
  78. package/memory-storage/resource-clients/key-value-store.d.ts +63 -0
  79. package/memory-storage/resource-clients/key-value-store.js +204 -0
  80. package/memory-storage/resource-clients/request-queue.d.ts +77 -0
  81. package/memory-storage/resource-clients/request-queue.js +422 -0
  82. package/memory-storage/utils.d.ts +16 -0
  83. package/memory-storage/utils.js +41 -0
  84. package/owned_or_injected.d.ts +58 -0
  85. package/owned_or_injected.js +98 -0
  86. package/package.json +13 -12
  87. package/proxy_configuration.d.ts +24 -132
  88. package/proxy_configuration.js +24 -143
  89. package/recoverable_state.d.ts +140 -0
  90. package/recoverable_state.js +212 -0
  91. package/request.d.ts +86 -17
  92. package/request.js +120 -41
  93. package/router.d.ts +193 -21
  94. package/router.js +188 -43
  95. package/serialization.d.ts +0 -1
  96. package/serialization.js +9 -11
  97. package/service_locator.d.ts +165 -0
  98. package/service_locator.js +253 -0
  99. package/session_pool/consts.d.ts +1 -2
  100. package/session_pool/consts.js +1 -2
  101. package/session_pool/errors.d.ts +0 -1
  102. package/session_pool/errors.js +0 -1
  103. package/session_pool/fingerprint.d.ts +9 -0
  104. package/session_pool/fingerprint.js +30 -0
  105. package/session_pool/index.d.ts +0 -2
  106. package/session_pool/index.js +0 -2
  107. package/session_pool/session.d.ts +35 -89
  108. package/session_pool/session.js +82 -142
  109. package/session_pool/session_pool.d.ts +69 -90
  110. package/session_pool/session_pool.js +151 -150
  111. package/storages/batched_adds.d.ts +37 -0
  112. package/storages/batched_adds.js +73 -0
  113. package/storages/dataset.d.ts +114 -54
  114. package/storages/dataset.js +285 -144
  115. package/storages/index.d.ts +10 -8
  116. package/storages/index.js +8 -8
  117. package/storages/key_value_store.d.ts +185 -42
  118. package/storages/key_value_store.js +424 -151
  119. package/storages/key_value_store_codec.d.ts +32 -0
  120. package/storages/key_value_store_codec.js +113 -0
  121. package/storages/request_dedup_cache.d.ts +22 -0
  122. package/storages/request_dedup_cache.js +48 -0
  123. package/storages/request_list.d.ts +52 -116
  124. package/storages/request_list.js +159 -133
  125. package/storages/request_loader.d.ts +101 -0
  126. package/storages/request_loader.js +1 -0
  127. package/storages/request_manager.d.ts +33 -0
  128. package/storages/request_manager.js +1 -0
  129. package/storages/request_manager_tandem.d.ts +97 -0
  130. package/storages/request_manager_tandem.js +197 -0
  131. package/storages/request_queue.d.ts +290 -47
  132. package/storages/request_queue.js +757 -216
  133. package/storages/{sitemap_request_list.d.ts → sitemap_request_loader.d.ts} +37 -88
  134. package/storages/{sitemap_request_list.js → sitemap_request_loader.js} +137 -143
  135. package/storages/storage_instance_manager.d.ts +87 -0
  136. package/storages/storage_instance_manager.js +256 -0
  137. package/storages/storage_stats.d.ts +48 -0
  138. package/storages/storage_stats.js +29 -0
  139. package/storages/throttling_request_manager.d.ts +216 -0
  140. package/storages/throttling_request_manager.js +453 -0
  141. package/storages/transaction.d.ts +252 -0
  142. package/storages/transaction.js +251 -0
  143. package/storages/utils.d.ts +58 -11
  144. package/storages/utils.js +64 -13
  145. package/system-info/cpu-info.d.ts +67 -0
  146. package/system-info/cpu-info.js +216 -0
  147. package/system-info/memory-info.d.ts +31 -0
  148. package/system-info/memory-info.js +115 -0
  149. package/system-info/ps-tree.d.ts +17 -0
  150. package/system-info/ps-tree.js +144 -0
  151. package/system-info/runtime.d.ts +14 -0
  152. package/system-info/runtime.js +80 -0
  153. package/typedefs.d.ts +0 -6
  154. package/typedefs.js +0 -1
  155. package/url.d.ts +9 -0
  156. package/url.js +11 -0
  157. package/validators.d.ts +8 -1
  158. package/validators.js +10 -3
  159. package/autoscaling/autoscaled_pool.d.ts.map +0 -1
  160. package/autoscaling/autoscaled_pool.js.map +0 -1
  161. package/autoscaling/index.d.ts.map +0 -1
  162. package/autoscaling/index.js.map +0 -1
  163. package/autoscaling/snapshotter.d.ts.map +0 -1
  164. package/autoscaling/snapshotter.js.map +0 -1
  165. package/autoscaling/system_status.d.ts.map +0 -1
  166. package/autoscaling/system_status.js.map +0 -1
  167. package/configuration.d.ts.map +0 -1
  168. package/configuration.js.map +0 -1
  169. package/cookie_utils.d.ts.map +0 -1
  170. package/cookie_utils.js.map +0 -1
  171. package/crawlers/context_pipeline.d.ts.map +0 -1
  172. package/crawlers/context_pipeline.js.map +0 -1
  173. package/crawlers/crawler_commons.d.ts.map +0 -1
  174. package/crawlers/crawler_commons.js.map +0 -1
  175. package/crawlers/crawler_utils.d.ts +0 -10
  176. package/crawlers/crawler_utils.d.ts.map +0 -1
  177. package/crawlers/crawler_utils.js +0 -12
  178. package/crawlers/crawler_utils.js.map +0 -1
  179. package/crawlers/error_snapshotter.d.ts.map +0 -1
  180. package/crawlers/error_snapshotter.js.map +0 -1
  181. package/crawlers/error_tracker.d.ts.map +0 -1
  182. package/crawlers/error_tracker.js.map +0 -1
  183. package/crawlers/index.d.ts.map +0 -1
  184. package/crawlers/index.js.map +0 -1
  185. package/crawlers/internals/types.d.ts.map +0 -1
  186. package/crawlers/internals/types.js.map +0 -1
  187. package/crawlers/statistics.d.ts.map +0 -1
  188. package/crawlers/statistics.js.map +0 -1
  189. package/enqueue_links/enqueue_links.d.ts.map +0 -1
  190. package/enqueue_links/enqueue_links.js.map +0 -1
  191. package/enqueue_links/index.d.ts.map +0 -1
  192. package/enqueue_links/index.js.map +0 -1
  193. package/enqueue_links/shared.d.ts.map +0 -1
  194. package/enqueue_links/shared.js.map +0 -1
  195. package/errors.d.ts.map +0 -1
  196. package/errors.js.map +0 -1
  197. package/events/event_manager.d.ts.map +0 -1
  198. package/events/event_manager.js.map +0 -1
  199. package/events/index.d.ts.map +0 -1
  200. package/events/index.js.map +0 -1
  201. package/events/local_event_manager.d.ts.map +0 -1
  202. package/events/local_event_manager.js.map +0 -1
  203. package/http_clients/base-http-client.d.ts +0 -134
  204. package/http_clients/base-http-client.d.ts.map +0 -1
  205. package/http_clients/base-http-client.js +0 -33
  206. package/http_clients/base-http-client.js.map +0 -1
  207. package/http_clients/form-data-like.d.ts +0 -67
  208. package/http_clients/form-data-like.d.ts.map +0 -1
  209. package/http_clients/form-data-like.js +0 -5
  210. package/http_clients/form-data-like.js.map +0 -1
  211. package/http_clients/got-scraping-http-client.d.ts +0 -15
  212. package/http_clients/got-scraping-http-client.d.ts.map +0 -1
  213. package/http_clients/got-scraping-http-client.js +0 -69
  214. package/http_clients/got-scraping-http-client.js.map +0 -1
  215. package/http_clients/index.d.ts +0 -3
  216. package/http_clients/index.d.ts.map +0 -1
  217. package/http_clients/index.js +0 -3
  218. package/http_clients/index.js.map +0 -1
  219. package/index.d.ts.map +0 -1
  220. package/index.js.map +0 -1
  221. package/log.d.ts.map +0 -1
  222. package/log.js.map +0 -1
  223. package/proxy_configuration.d.ts.map +0 -1
  224. package/proxy_configuration.js.map +0 -1
  225. package/request.d.ts.map +0 -1
  226. package/request.js.map +0 -1
  227. package/router.d.ts.map +0 -1
  228. package/router.js.map +0 -1
  229. package/serialization.d.ts.map +0 -1
  230. package/serialization.js.map +0 -1
  231. package/session_pool/consts.d.ts.map +0 -1
  232. package/session_pool/consts.js.map +0 -1
  233. package/session_pool/errors.d.ts.map +0 -1
  234. package/session_pool/errors.js.map +0 -1
  235. package/session_pool/events.d.ts +0 -3
  236. package/session_pool/events.d.ts.map +0 -1
  237. package/session_pool/events.js +0 -3
  238. package/session_pool/events.js.map +0 -1
  239. package/session_pool/index.d.ts.map +0 -1
  240. package/session_pool/index.js.map +0 -1
  241. package/session_pool/session.d.ts.map +0 -1
  242. package/session_pool/session.js.map +0 -1
  243. package/session_pool/session_pool.d.ts.map +0 -1
  244. package/session_pool/session_pool.js.map +0 -1
  245. package/storages/access_checking.d.ts +0 -13
  246. package/storages/access_checking.d.ts.map +0 -1
  247. package/storages/access_checking.js +0 -14
  248. package/storages/access_checking.js.map +0 -1
  249. package/storages/dataset.d.ts.map +0 -1
  250. package/storages/dataset.js.map +0 -1
  251. package/storages/index.d.ts.map +0 -1
  252. package/storages/index.js.map +0 -1
  253. package/storages/key_value_store.d.ts.map +0 -1
  254. package/storages/key_value_store.js.map +0 -1
  255. package/storages/request_list.d.ts.map +0 -1
  256. package/storages/request_list.js.map +0 -1
  257. package/storages/request_provider.d.ts +0 -308
  258. package/storages/request_provider.d.ts.map +0 -1
  259. package/storages/request_provider.js +0 -555
  260. package/storages/request_provider.js.map +0 -1
  261. package/storages/request_queue.d.ts.map +0 -1
  262. package/storages/request_queue.js.map +0 -1
  263. package/storages/request_queue_v2.d.ts +0 -87
  264. package/storages/request_queue_v2.d.ts.map +0 -1
  265. package/storages/request_queue_v2.js +0 -438
  266. package/storages/request_queue_v2.js.map +0 -1
  267. package/storages/sitemap_request_list.d.ts.map +0 -1
  268. package/storages/sitemap_request_list.js.map +0 -1
  269. package/storages/storage_manager.d.ts +0 -58
  270. package/storages/storage_manager.d.ts.map +0 -1
  271. package/storages/storage_manager.js +0 -105
  272. package/storages/storage_manager.js.map +0 -1
  273. package/storages/utils.d.ts.map +0 -1
  274. package/storages/utils.js.map +0 -1
  275. package/tsconfig.build.tsbuildinfo +0 -1
  276. package/typedefs.d.ts.map +0 -1
  277. package/typedefs.js.map +0 -1
  278. package/validators.d.ts.map +0 -1
  279. package/validators.js.map +0 -1
@@ -1,20 +1,20 @@
1
1
  import ow from 'ow';
2
- import { Configuration } from '../configuration.js';
3
- import { log as defaultLog } from '../log.js';
4
- import { KeyValueStore } from '../storages/key_value_store.js';
2
+ import { z } from 'zod';
3
+ import { RecoverableState } from '../recoverable_state.js';
4
+ import { serviceLocator } from '../service_locator.js';
5
5
  import { ErrorTracker } from './error_tracker.js';
6
6
  /**
7
7
  * @ignore
8
8
  */
9
9
  class Job {
10
- lastRunAt = null;
11
- durationMillis;
10
+ #lastRunAt = null;
11
+ #durationMillis;
12
12
  run() {
13
- this.lastRunAt = Date.now();
13
+ this.#lastRunAt = Date.now();
14
14
  }
15
15
  finish() {
16
- this.durationMillis = Date.now() - this.lastRunAt;
17
- return this.durationMillis;
16
+ this.#durationMillis = Date.now() - this.#lastRunAt;
17
+ return this.#durationMillis;
18
18
  }
19
19
  }
20
20
  const errorTrackerConfig = {
@@ -25,17 +25,127 @@ const errorTrackerConfig = {
25
25
  showErrorMessage: true,
26
26
  showFullMessage: false,
27
27
  };
28
+ /**
29
+ * The persisted record, in the order it is written - the schema rebuilds the object on the way out, so the field
30
+ * order here *is* the record's field order (guarded by a test).
31
+ *
32
+ * JSON has no infinity, so the three fields that are `Infinity` until the first request settles are written as
33
+ * `null`. Both {@link Statistics.serializeState} and {@link Statistics.deserializeState} run through this,
34
+ * which is what keeps them describing the same record.
35
+ *
36
+ * Nothing is optional on purpose: `serializeState` has always written every field, so a record missing one is not
37
+ * one of ours and is discarded whole rather than partially trusted - a counter restored as a string would poison
38
+ * every later increment.
39
+ */
40
+ const persistedStatisticState = z
41
+ .object({
42
+ requestsFinished: z.number(),
43
+ requestsFailed: z.number(),
44
+ requestsRetries: z.number(),
45
+ requestsFailedPerMinute: z.number().nullable(),
46
+ requestsFinishedPerMinute: z.number().nullable(),
47
+ requestMinDurationMillis: z.number().nullable(),
48
+ requestMaxDurationMillis: z.number(),
49
+ requestTotalFailedDurationMillis: z.number(),
50
+ requestTotalFinishedDurationMillis: z.number(),
51
+ crawlerStartedAt: z.string().nullable(),
52
+ crawlerFinishedAt: z.string().nullable(),
53
+ statsPersistedAt: z.string(),
54
+ crawlerRuntimeMillis: z.number(),
55
+ crawlerLastStartTimestamp: z.number(),
56
+ // A retry count that never occurred leaves a hole in the live histogram, written out as a `null`. We
57
+ // once saw a record whose histogram was not an array at all and crashed the crawler on load.
58
+ requestRetryHistogram: z.array(z.number().nullable()),
59
+ statsId: z.string(),
60
+ requestAvgFailedDurationMillis: z.number().nullable(),
61
+ requestAvgFinishedDurationMillis: z.number().nullable(),
62
+ requestTotalDurationMillis: z.number(),
63
+ requestsTotal: z.number(),
64
+ requestsWithStatusCode: z.record(z.string(), z.number()),
65
+ errors: z.record(z.string(), z.unknown()),
66
+ retryErrors: z.record(z.string(), z.unknown()),
67
+ })
68
+ // A subclass tracking extra fields spreads them into the record; they are none of this schema's business,
69
+ // but they must not be dropped on the way through it.
70
+ .catchall(z.unknown());
71
+ /** `Infinity` is what the statistics use for "nothing to average yet"; JSON has only `null` for it. */
72
+ function finiteOrNull(value) {
73
+ return Number.isFinite(value) ? value : null;
74
+ }
75
+ /**
76
+ * The conversion between the live state and the record above, in both directions.
77
+ *
78
+ * Built per instance rather than kept as a constant because a record carries three things the state does not: the
79
+ * instance `id`, the derived aggregates of the overridable {@link Statistics.calculate}, and - on the way back -
80
+ * the fields that are rebuilt from {@link Statistics.defaultState} rather than restored, the error trackers
81
+ * among them.
82
+ *
83
+ * The model side is deliberately opaque: zod rebuilds what it validates, and `state.errors` has to stay the very
84
+ * object the error trackers write into, not a copy of it.
85
+ */
86
+ function buildStatisticStateCodec(statistics) {
87
+ return z.codec(persistedStatisticState, z.custom(), {
88
+ decode: (record) => ({
89
+ ...statistics.defaultState(),
90
+ requestsFinished: record.requestsFinished,
91
+ requestsFailed: record.requestsFailed,
92
+ requestsRetries: record.requestsRetries,
93
+ requestTotalFailedDurationMillis: record.requestTotalFailedDurationMillis,
94
+ requestTotalFinishedDurationMillis: record.requestTotalFinishedDurationMillis,
95
+ // Restoring the `null` as-is would make every later `duration < min` comparison fail, leaving the
96
+ // minimum `null` for the rest of the run.
97
+ requestMinDurationMillis: record.requestMinDurationMillis ?? Infinity,
98
+ requestMaxDurationMillis: record.requestMaxDurationMillis,
99
+ crawlerRuntimeMillis: record.crawlerRuntimeMillis,
100
+ // A `null` stands for the zero requests that reached that retry count - restore it as such.
101
+ requestRetryHistogram: record.requestRetryHistogram.map((count) => count ?? 0),
102
+ // The record keeps ISO strings, the live state keeps `Date`s.
103
+ crawlerStartedAt: record.crawlerStartedAt === null ? null : new Date(record.crawlerStartedAt),
104
+ crawlerFinishedAt: record.crawlerFinishedAt === null ? null : new Date(record.crawlerFinishedAt),
105
+ statsPersistedAt: new Date(record.statsPersistedAt),
106
+ // Rebased so that the runtime reported by `calculate()` spans the migration instead of restarting.
107
+ instanceStart: Date.now() - (new Date(record.statsPersistedAt).getTime() - record.crawlerLastStartTimestamp),
108
+ }),
109
+ encode: (state) => {
110
+ const { requestsWithStatusCode, errors, retryErrors, requestRetryHistogram, instanceStart, ...counters } = state;
111
+ // Every rate and average `calculate()` derives is `Infinity` until the run is long enough, or until
112
+ // something has finished or failed, to divide by.
113
+ const { requestAvgFailedDurationMillis, requestAvgFinishedDurationMillis, requestsFailedPerMinute, requestsFinishedPerMinute, ...aggregates } = statistics.calculate();
114
+ return {
115
+ ...counters,
116
+ requestMinDurationMillis: finiteOrNull(state.requestMinDurationMillis),
117
+ crawlerStartedAt: state.crawlerStartedAt ? new Date(state.crawlerStartedAt).toISOString() : null,
118
+ crawlerFinishedAt: state.crawlerFinishedAt ? new Date(state.crawlerFinishedAt).toISOString() : null,
119
+ statsPersistedAt: new Date().toISOString(),
120
+ crawlerLastStartTimestamp: instanceStart,
121
+ // `Array.from`, not `map` - a hole left by a retry count no request reached is skipped by `map`
122
+ // and would stay a hole, which is not something the record can carry.
123
+ requestRetryHistogram: Array.from(requestRetryHistogram, (count) => count ?? null),
124
+ statsId: statistics.statsId,
125
+ ...aggregates,
126
+ requestAvgFailedDurationMillis: finiteOrNull(requestAvgFailedDurationMillis),
127
+ requestAvgFinishedDurationMillis: finiteOrNull(requestAvgFinishedDurationMillis),
128
+ requestsFailedPerMinute: finiteOrNull(requestsFailedPerMinute),
129
+ requestsFinishedPerMinute: finiteOrNull(requestsFinishedPerMinute),
130
+ requestsWithStatusCode,
131
+ errors,
132
+ retryErrors,
133
+ };
134
+ },
135
+ });
136
+ }
28
137
  /**
29
138
  * The statistics class provides an interface to collecting and logging run
30
139
  * statistics for requests.
31
140
  *
32
141
  * All statistic information is saved on key value store
33
- * under the key `SDK_CRAWLER_STATISTICS_*`, persists between
142
+ * under the key `CRAWLEE_CRAWLER_STATISTICS_*`, persists between
34
143
  * migrations and abort/resurrect
35
144
  *
36
145
  * @category Crawlers
37
146
  */
38
147
  export class Statistics {
148
+ // kept as TS-private: statistics tests read the static counter directly
39
149
  static id = 0;
40
150
  /**
41
151
  * An error tracker for final retry errors.
@@ -48,32 +158,30 @@ export class Statistics {
48
158
  /**
49
159
  * Statistic instance id.
50
160
  */
51
- id = Statistics.id++; // assign an id while incrementing so it can be saved/restored from KV
161
+ id;
162
+ persistStateKey;
163
+ #stateCodec;
164
+ #recoverableState;
165
+ #logIntervalMillis;
166
+ #logMessage;
167
+ #requestsInProgress = new Map();
168
+ log;
169
+ #logInterval;
52
170
  /**
53
171
  * Current statistic state used for doing calculations on {@link Statistics.calculate} calls
54
172
  */
55
- state;
173
+ get state() {
174
+ return this.#recoverableState.currentValue;
175
+ }
56
176
  /**
57
177
  * Contains the current retries histogram. Index 0 means 0 retries, index 2, 2 retries, and so on
58
178
  */
59
- requestRetryHistogram = [];
60
- /**
61
- * Contains the associated Configuration instance
62
- */
63
- config;
64
- keyValueStore = undefined;
65
- persistStateKey = `SDK_CRAWLER_STATISTICS_${this.id}`;
66
- logIntervalMillis;
67
- logMessage;
68
- listener;
69
- requestsInProgress = new Map();
70
- log;
71
- instanceStart;
72
- logInterval;
73
- events;
74
- persistenceOptions;
179
+ get requestRetryHistogram() {
180
+ return this.state.requestRetryHistogram;
181
+ }
75
182
  /**
76
- * @internal
183
+ * Construct a statistics instance to pass to a crawler via its `statistics` option, e.g. to preconfigure
184
+ * persistence or error snapshots, share it across sequential runs, or subclass it to track extra fields.
77
185
  */
78
186
  constructor(options = {}) {
79
187
  ow(options, ow.object.exactShape({
@@ -81,33 +189,56 @@ export class Statistics {
81
189
  logMessage: ow.optional.string,
82
190
  log: ow.optional.object,
83
191
  keyValueStore: ow.optional.object,
84
- config: ow.optional.object,
85
192
  persistenceOptions: ow.optional.object,
86
193
  saveErrorSnapshots: ow.optional.boolean,
194
+ id: ow.optional.any(ow.number, ow.string),
87
195
  }));
88
- const { logIntervalSecs = 60, logMessage = 'Statistics', keyValueStore, config = Configuration.getGlobalConfig(), persistenceOptions = {
196
+ const { logIntervalSecs = 60, logMessage = 'Statistics', keyValueStore, persistenceOptions = {
89
197
  enable: true,
90
- }, saveErrorSnapshots = false, } = options;
91
- this.log = (options.log ?? defaultLog).child({ prefix: 'Statistics' });
198
+ }, saveErrorSnapshots = false, id, } = options;
199
+ this.id = id ?? String(Statistics.id++);
200
+ this.persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
201
+ this.log = (options.log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
92
202
  this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
93
203
  this.errorTrackerRetry = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
94
- this.logIntervalMillis = logIntervalSecs * 1000;
95
- this.logMessage = logMessage;
96
- this.keyValueStore = keyValueStore;
97
- this.listener = this.persistState.bind(this);
98
- this.events = config.getEventManager();
99
- this.config = config;
100
- this.persistenceOptions = persistenceOptions;
204
+ this.#logIntervalMillis = logIntervalSecs * 1000;
205
+ this.#logMessage = logMessage;
206
+ // Late-bound on purpose - both hooks are override points, and a subclass's must be the ones that run.
207
+ this.#stateCodec = buildStatisticStateCodec({
208
+ statsId: this.id,
209
+ defaultState: () => this.defaultState(),
210
+ calculate: () => this.calculate(),
211
+ });
212
+ this.#recoverableState = new RecoverableState({
213
+ persistStateKey: this.persistStateKey,
214
+ persistenceEnabled: persistenceOptions.enable,
215
+ keyValueStore,
216
+ logger: this.log,
217
+ defaultState: () => this.defaultState(),
218
+ serialize: (state) => this.serializeState(state),
219
+ deserialize: (persistedState) => this.deserializeState(persistedState),
220
+ });
101
221
  // initialize by "resetting"
102
222
  this.reset();
103
223
  }
104
224
  /**
105
- * Set the current statistic instance to pristine values
225
+ * Set the current statistic instance to pristine values.
226
+ *
227
+ * The persisted record is left alone - use {@link Statistics.resetStore} to clear that as well.
106
228
  */
107
229
  reset() {
108
230
  this.errorTracker.reset();
109
231
  this.errorTrackerRetry.reset();
110
- this.state = {
232
+ this.#recoverableState.reset();
233
+ this.#requestsInProgress.clear();
234
+ }
235
+ /**
236
+ * The pristine state a new instance starts with and {@link Statistics.reset} restores.
237
+ *
238
+ * A subclass tracking extra fields declares their initial values here.
239
+ */
240
+ defaultState() {
241
+ return {
111
242
  requestsFinished: 0,
112
243
  requestsFailed: 0,
113
244
  requestsRetries: 0,
@@ -122,25 +253,20 @@ export class Statistics {
122
253
  statsPersistedAt: null,
123
254
  crawlerRuntimeMillis: 0,
124
255
  requestsWithStatusCode: {},
256
+ // Aliases, not copies - the trackers keep writing into these objects.
125
257
  errors: this.errorTracker.result,
126
258
  retryErrors: this.errorTrackerRetry.result,
259
+ requestRetryHistogram: [],
260
+ instanceStart: Date.now(),
127
261
  };
128
- this.requestRetryHistogram.length = 0;
129
- this.requestsInProgress.clear();
130
- this.instanceStart = Date.now();
131
- this._teardown();
132
262
  }
133
263
  /**
134
- * @param options - Override the persistence options provided in the constructor
264
+ * Clear the persisted statistics record, leaving the in-memory state alone.
265
+ *
266
+ * Throws while capturing - the next PERSIST_STATE event would write the record straight back.
135
267
  */
136
- async resetStore(options) {
137
- if (!this.persistenceOptions.enable && !options?.enable) {
138
- return;
139
- }
140
- if (!this.keyValueStore) {
141
- return;
142
- }
143
- await this.keyValueStore.setValue(this.persistStateKey, null);
268
+ async resetStore() {
269
+ await this.#recoverableState.resetStore();
144
270
  }
145
271
  /**
146
272
  * Increments the status code counter.
@@ -157,49 +283,57 @@ export class Statistics {
157
283
  * @ignore
158
284
  */
159
285
  startJob(id) {
160
- let job = this.requestsInProgress.get(id);
286
+ let job = this.#requestsInProgress.get(id);
161
287
  if (!job)
162
288
  job = new Job();
163
289
  job.run();
164
- this.requestsInProgress.set(id, job);
290
+ this.#requestsInProgress.set(id, job);
165
291
  }
166
292
  /**
167
293
  * Mark job as finished and sets the state
168
294
  * @ignore
169
295
  */
170
296
  finishJob(id, retryCount) {
171
- const job = this.requestsInProgress.get(id);
297
+ const job = this.#requestsInProgress.get(id);
172
298
  if (!job)
173
299
  return;
174
300
  const jobDurationMillis = job.finish();
175
301
  this.state.requestsFinished++;
176
302
  this.state.requestTotalFinishedDurationMillis += jobDurationMillis;
177
- this._saveRetryCountForJob(retryCount);
303
+ this.saveRetryCountForJob(retryCount);
178
304
  if (jobDurationMillis < this.state.requestMinDurationMillis)
179
305
  this.state.requestMinDurationMillis = jobDurationMillis;
180
306
  if (jobDurationMillis > this.state.requestMaxDurationMillis)
181
307
  this.state.requestMaxDurationMillis = jobDurationMillis;
182
- this.requestsInProgress.delete(id);
308
+ this.#requestsInProgress.delete(id);
183
309
  }
184
310
  /**
185
311
  * Mark job as failed and sets the state
186
312
  * @ignore
187
313
  */
188
314
  failJob(id, retryCount) {
189
- const job = this.requestsInProgress.get(id);
315
+ const job = this.#requestsInProgress.get(id);
190
316
  if (!job)
191
317
  return;
192
318
  this.state.requestTotalFailedDurationMillis += job.finish();
193
319
  this.state.requestsFailed++;
194
- this._saveRetryCountForJob(retryCount);
195
- this.requestsInProgress.delete(id);
320
+ this.saveRetryCountForJob(retryCount);
321
+ this.#requestsInProgress.delete(id);
322
+ }
323
+ /**
324
+ * Discards a started job without affecting the finished/failed counters, e.g. when a request
325
+ * turns out to be skipped (robots.txt, enqueue strategy) after `startJob` was already called for it.
326
+ * @ignore
327
+ */
328
+ discardJob(id) {
329
+ this.#requestsInProgress.delete(id);
196
330
  }
197
331
  /**
198
332
  * Calculate the current statistics
199
333
  */
200
334
  calculate() {
201
335
  const { requestsFailed, requestsFinished, requestTotalFailedDurationMillis, requestTotalFinishedDurationMillis, } = this.state;
202
- const totalMillis = Date.now() - this.instanceStart;
336
+ const totalMillis = Date.now() - this.state.instanceStart;
203
337
  const totalMinutes = totalMillis / 1000 / 60;
204
338
  return {
205
339
  requestAvgFailedDurationMillis: Math.round(requestTotalFailedDurationMillis / requestsFailed) || Infinity,
@@ -216,131 +350,87 @@ export class Statistics {
216
350
  * displaying the current state in predefined intervals
217
351
  */
218
352
  async startCapturing() {
219
- this.keyValueStore ??= await KeyValueStore.open(null, { config: this.config });
353
+ // A single instance drives one logging interval and one PERSIST_STATE listener, so a second concurrent
354
+ // capture (e.g. sharing one instance across crawlers running at once) would orphan the first. Fail loudly.
355
+ if (this.#logInterval) {
356
+ throw new Error('Statistics.startCapturing() was already called - this instance is already capturing.');
357
+ }
358
+ await this.#recoverableState.initialize();
359
+ // After the load, so that a restored record keeps the timestamp of the run it belongs to.
220
360
  if (this.state.crawlerStartedAt === null) {
221
361
  this.state.crawlerStartedAt = new Date();
222
362
  }
223
- if (this.persistenceOptions.enable) {
224
- await this._maybeLoadStatistics();
225
- this.events.on("persistState" /* EventType.PERSIST_STATE */, this.listener);
226
- }
227
- this.logInterval = setInterval(() => {
228
- this.log.info(this.logMessage, {
363
+ this.#logInterval = setInterval(() => {
364
+ this.log.info(this.#logMessage, {
229
365
  ...this.calculate(),
230
366
  retryHistogram: this.requestRetryHistogram,
231
367
  });
232
- }, this.logIntervalMillis);
368
+ }, this.#logIntervalMillis);
233
369
  }
234
370
  /**
235
371
  * Stops logging and remove event listeners, then persist
236
372
  */
237
373
  async stopCapturing() {
238
- this._teardown();
374
+ this.#stopLogging();
239
375
  this.state.crawlerFinishedAt = new Date();
240
- await this.persistState();
376
+ await this.#recoverableState.teardown();
241
377
  }
242
- _saveRetryCountForJob(retryCount) {
378
+ saveRetryCountForJob(retryCount) {
243
379
  if (retryCount > 0)
244
380
  this.state.requestsRetries++;
245
381
  this.requestRetryHistogram[retryCount] ??= 0;
246
382
  this.requestRetryHistogram[retryCount]++;
247
383
  }
248
384
  /**
249
- * Persist internal state to the key value store
250
- * @param options - Override the persistence options provided in the constructor
385
+ * Persist internal state to the key value store.
386
+ *
387
+ * Statistics are bookkeeping - a store that refuses the write is worth a warning, not a failed crawl. The
388
+ * crawler calls this from its migration handler, where a rejection would go unhandled.
251
389
  */
252
- async persistState(options) {
253
- if (!this.persistenceOptions.enable && !options?.enable) {
254
- return;
255
- }
256
- // this might be called before startCapturing was called without using await, should not crash
257
- if (!this.keyValueStore) {
258
- return;
259
- }
260
- this.log.debug('Persisting state', { persistStateKey: this.persistStateKey });
261
- // use half the interval of `persistState` to avoid race conditions
262
- const persistStateIntervalMillis = this.config.get('persistStateIntervalMillis');
263
- const timeoutSecs = persistStateIntervalMillis / 2_000;
264
- await this.keyValueStore
265
- .setValue(this.persistStateKey, this.toJSON(), {
266
- timeoutSecs,
267
- doNotRetryTimeouts: true,
268
- })
390
+ async persistState() {
391
+ await this.#recoverableState
392
+ .persistState()
269
393
  .catch((error) => this.log.warning(`Failed to persist the statistics to ${this.persistStateKey}`, { error }));
270
394
  }
271
395
  /**
272
- * Loads the current statistic from the key value store if any
396
+ * Rebuilds the state from a persisted record.
397
+ *
398
+ * A subclass tracking extra fields restores them here, on top of the result of `super.deserializeState()`.
273
399
  */
274
- async _maybeLoadStatistics() {
275
- // this might be called before startCapturing was called without using await, should not crash
276
- if (!this.keyValueStore) {
277
- return;
278
- }
279
- const savedState = await this.keyValueStore.getValue(this.persistStateKey);
280
- if (!savedState)
281
- return;
282
- // We saw a run where the requestRetryHistogram was not iterable and crashed
283
- // the crawler. Adding some logging to monitor this problem in the future.
284
- if (!Array.isArray(savedState.requestRetryHistogram)) {
285
- this.log.warning('Received invalid state from Key-value store.', {
400
+ deserializeState(persistedState) {
401
+ // The cast is the index signature the `catchall` puts on the schema and an interface cannot have - the
402
+ // record is an unvalidated blob off the key-value store either way, which is what the decode is for.
403
+ const restored = z.safeDecode(this.#stateCodec, persistedState);
404
+ if (!restored.success) {
405
+ // Statistics are bookkeeping - a record that cannot be made sense of is worth a warning and a fresh
406
+ // start, not a failed crawl.
407
+ this.log.warning('Received invalid state from Key-value store, starting the statistics from scratch.', {
286
408
  persistStateKey: this.persistStateKey,
287
- state: savedState,
409
+ issues: restored.error.issues,
288
410
  });
411
+ return this.defaultState();
289
412
  }
290
413
  this.log.debug('Recreating state from KeyValueStore', { persistStateKey: this.persistStateKey });
291
- // the `requestRetryHistogram` array might be very large, we could end up with
292
- // `RangeError: Maximum call stack size exceeded` if we use `a.push(...b)`
293
- savedState.requestRetryHistogram.forEach((idx) => this.requestRetryHistogram.push(idx));
294
- this.state.requestsFinished = savedState.requestsFinished;
295
- this.state.requestsFailed = savedState.requestsFailed;
296
- this.state.requestsRetries = savedState.requestsRetries;
297
- this.state.requestTotalFailedDurationMillis = savedState.requestTotalFailedDurationMillis;
298
- this.state.requestTotalFinishedDurationMillis = savedState.requestTotalFinishedDurationMillis;
299
- this.state.requestMinDurationMillis = savedState.requestMinDurationMillis;
300
- this.state.requestMaxDurationMillis = savedState.requestMaxDurationMillis;
301
- // persisted state uses ISO date strings
302
- this.state.crawlerFinishedAt = savedState.crawlerFinishedAt ? new Date(savedState.crawlerFinishedAt) : null;
303
- this.state.crawlerStartedAt = savedState.crawlerStartedAt ? new Date(savedState.crawlerStartedAt) : null;
304
- this.state.statsPersistedAt = savedState.statsPersistedAt ? new Date(savedState.statsPersistedAt) : null;
305
- this.state.crawlerRuntimeMillis = savedState.crawlerRuntimeMillis;
306
- this.instanceStart = Date.now() - (+this.state.statsPersistedAt - savedState.crawlerLastStartTimestamp);
307
- this.log.debug('Loaded from KeyValueStore');
414
+ return { ...this.defaultState(), ...restored.data };
308
415
  }
309
- _teardown() {
310
- // this can be called before a call to startCapturing happens (or in a 'finally' block)
311
- this.events.off("persistState" /* EventType.PERSIST_STATE */, this.listener);
312
- if (this.logInterval) {
313
- clearInterval(this.logInterval);
314
- this.logInterval = null;
416
+ #stopLogging() {
417
+ if (this.#logInterval) {
418
+ clearInterval(this.#logInterval);
419
+ this.#logInterval = null;
315
420
  }
316
421
  }
422
+ /**
423
+ * Builds the record written to the key value store, merging in the derived aggregates so that a consumer
424
+ * reading the record does not have to reconstruct them.
425
+ */
426
+ serializeState(state) {
427
+ return z.encode(this.#stateCodec, state);
428
+ }
317
429
  /**
318
430
  * Make this class serializable when called with `JSON.stringify(statsInstance)` directly
319
431
  * or through `keyValueStore.setValue('KEY', statsInstance)`
320
432
  */
321
433
  toJSON() {
322
- // merge all the current state information that can be used from the outside
323
- // without the need to reconstruct for the sake of stats.calculate()
324
- // omit duplicated information
325
- const result = {
326
- ...this.state,
327
- crawlerLastStartTimestamp: this.instanceStart,
328
- crawlerFinishedAt: this.state.crawlerFinishedAt
329
- ? new Date(this.state.crawlerFinishedAt).toISOString()
330
- : null,
331
- crawlerStartedAt: this.state.crawlerStartedAt ? new Date(this.state.crawlerStartedAt).toISOString() : null,
332
- requestRetryHistogram: this.requestRetryHistogram,
333
- statsId: this.id,
334
- statsPersistedAt: new Date().toISOString(),
335
- ...this.calculate(),
336
- };
337
- Reflect.deleteProperty(result, 'requestsWithStatusCode');
338
- Reflect.deleteProperty(result, 'errors');
339
- Reflect.deleteProperty(result, 'retryErrors');
340
- result.requestsWithStatusCode = this.state.requestsWithStatusCode;
341
- result.errors = this.state.errors;
342
- result.retryErrors = this.state.retryErrors;
343
- return result;
434
+ return this.serializeState(this.state);
344
435
  }
345
436
  }
346
- //# sourceMappingURL=statistics.js.map
package/debug.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { Dictionary } from '@crawlee/types';
3
+ import type { Request } from './request.js';
4
+ interface BrowserResponseLike {
5
+ status(): number;
6
+ }
7
+ /**
8
+ * Creates a standardized debug info from request and response. This info is usually added to dataset under the hidden `#debug` field.
9
+ *
10
+ * @param request [Request](https://sdk.apify.com/docs/api/request) object.
11
+ * @param [response]
12
+ * Puppeteer [`Response`](https://pptr.dev/#?product=Puppeteer&version=v1.11.0&show=api-class-response)
13
+ * or NodeJS [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_serverresponse).
14
+ * @param [additionalFields] Object containing additional fields to be added.
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function createRequestDebugInfo(request: Request, response?: IncomingMessage | Partial<BrowserResponseLike>, additionalFields?: Dictionary): Dictionary;
19
+ /**
20
+ * Returns a human-readable label for an unknown value,
21
+ * suitable for embedding in error messages and log output.
22
+ *
23
+ * Returns `constructor.name` when available (e.g. `"Configuration"`, `"Number"`),
24
+ * otherwise falls back to `util.inspect` (e.g. for `null`, `undefined`).
25
+ *
26
+ * @internal
27
+ */
28
+ export declare function inspectValue(value: unknown): string;
29
+ /**
30
+ * Returns the type of a value as a lowercase string, with `Date`, `Buffer` and `RegExp` reported
31
+ * by their constructor name. Used for building validation error messages.
32
+ *
33
+ * @internal
34
+ */
35
+ export declare function getObjectType(value: unknown): string;
36
+ export {};
package/debug.js ADDED
@@ -0,0 +1,70 @@
1
+ import { inspect } from 'node:util';
2
+ import ow from 'ow';
3
+ /**
4
+ * Creates a standardized debug info from request and response. This info is usually added to dataset under the hidden `#debug` field.
5
+ *
6
+ * @param request [Request](https://sdk.apify.com/docs/api/request) object.
7
+ * @param [response]
8
+ * Puppeteer [`Response`](https://pptr.dev/#?product=Puppeteer&version=v1.11.0&show=api-class-response)
9
+ * or NodeJS [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_serverresponse).
10
+ * @param [additionalFields] Object containing additional fields to be added.
11
+ *
12
+ * @internal
13
+ */
14
+ export function createRequestDebugInfo(request, response = {}, additionalFields = {}) {
15
+ ow(request, ow.object);
16
+ ow(response, ow.object);
17
+ ow(additionalFields, ow.object);
18
+ return {
19
+ requestId: request.id,
20
+ url: request.url,
21
+ loadedUrl: request.loadedUrl,
22
+ method: request.method,
23
+ retryCount: request.retryCount,
24
+ errorMessages: request.errorMessages,
25
+ // Puppeteer response has .status() function and NodeJS response, statusCode property.
26
+ statusCode: 'status' in response && response.status instanceof Function
27
+ ? response.status()
28
+ : response.statusCode,
29
+ ...additionalFields,
30
+ };
31
+ }
32
+ /**
33
+ * Returns a human-readable label for an unknown value,
34
+ * suitable for embedding in error messages and log output.
35
+ *
36
+ * Returns `constructor.name` when available (e.g. `"Configuration"`, `"Number"`),
37
+ * otherwise falls back to `util.inspect` (e.g. for `null`, `undefined`).
38
+ *
39
+ * @internal
40
+ */
41
+ export function inspectValue(value) {
42
+ if (typeof value === 'object' && value !== null && value.constructor?.name) {
43
+ return value.constructor.name;
44
+ }
45
+ return inspect(value, {
46
+ depth: 0,
47
+ compact: true,
48
+ maxStringLength: 64,
49
+ breakLength: Infinity,
50
+ colors: false,
51
+ });
52
+ }
53
+ /**
54
+ * Returns the type of a value as a lowercase string, with `Date`, `Buffer` and `RegExp` reported
55
+ * by their constructor name. Used for building validation error messages.
56
+ *
57
+ * @internal
58
+ */
59
+ export function getObjectType(value) {
60
+ const simple = typeof value;
61
+ if (['string', 'number', 'boolean', 'bigint'].includes(simple)) {
62
+ return simple;
63
+ }
64
+ const objectType = Object.prototype.toString.call(value);
65
+ const type = /\[object (\w+)]/.exec(objectType)[1];
66
+ if (type === 'Uint8Array') {
67
+ return 'Buffer';
68
+ }
69
+ return ['Date', 'Buffer', 'RegExp'].includes(type) ? type : type.toLowerCase();
70
+ }