seo 0.2.16 → 0.2.17
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.
- package/dist/cli.js +24 -24
- package/dist/cli.js.map +1 -1
- package/dist/{dist-BeDz2qIL.js → dist-B36ZRY2E.js} +226 -50
- package/dist/dist-B36ZRY2E.js.map +1 -0
- package/dist/{dist-Hd0YW5ui.js → dist-B9Clu4T_.js} +1 -1
- package/dist/index.d.ts +355 -2
- package/dist/index.js +1819 -502
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1772 -382
- package/dist/mcp.js.map +1 -1
- package/dist/{package-D_ix15KL.js → package-C-Oa9Mgz.js} +2 -2
- package/dist/{package-D_ix15KL.js.map → package-C-Oa9Mgz.js.map} +1 -1
- package/evals/rank-tracking.json +40 -0
- package/package.json +2 -1
- package/skills/seo/SKILL.md +1 -0
- package/dist/dist-BeDz2qIL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as load, t as version } from "./package-
|
|
2
|
+
import { n as load, t as version } from "./package-C-Oa9Mgz.js";
|
|
3
3
|
import { Headers, fetch as fetch$1 } from "undici";
|
|
4
4
|
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
@@ -1148,6 +1148,111 @@ CREATE INDEX IF NOT EXISTS idx_keyword_set_tags_lookup
|
|
|
1148
1148
|
ON keyword_set_tags(set_id, tag, normalized_keyword);
|
|
1149
1149
|
`;
|
|
1150
1150
|
//#endregion
|
|
1151
|
+
//#region packages/core/src/rank-tracking/schema.ts
|
|
1152
|
+
const RANK_TRACKING_SCHEMA_SQL = `
|
|
1153
|
+
CREATE TABLE IF NOT EXISTS rank_tracking_configs (
|
|
1154
|
+
id TEXT PRIMARY KEY,
|
|
1155
|
+
config_key TEXT NOT NULL UNIQUE,
|
|
1156
|
+
project_id TEXT NOT NULL,
|
|
1157
|
+
keyword_set_id TEXT NOT NULL,
|
|
1158
|
+
target_domain TEXT NOT NULL,
|
|
1159
|
+
tag TEXT,
|
|
1160
|
+
market_json TEXT NOT NULL,
|
|
1161
|
+
devices_json TEXT NOT NULL,
|
|
1162
|
+
provider TEXT NOT NULL CHECK(provider IN ('dataforseo', 'semrush', 'ahrefs')),
|
|
1163
|
+
collection_method TEXT NOT NULL CHECK(collection_method IN ('live', 'queued')),
|
|
1164
|
+
cadence TEXT NOT NULL CHECK(cadence IN ('manual', 'daily', 'weekly', 'monthly')),
|
|
1165
|
+
depth INTEGER NOT NULL CHECK(depth BETWEEN 1 AND 100),
|
|
1166
|
+
keyword_limit INTEGER NOT NULL CHECK(keyword_limit BETWEEN 1 AND 1000),
|
|
1167
|
+
next_run_at INTEGER,
|
|
1168
|
+
created_at INTEGER NOT NULL,
|
|
1169
|
+
updated_at INTEGER NOT NULL,
|
|
1170
|
+
FOREIGN KEY(keyword_set_id) REFERENCES keyword_sets(id) ON DELETE CASCADE
|
|
1171
|
+
);
|
|
1172
|
+
CREATE INDEX IF NOT EXISTS idx_rank_tracking_configs_project
|
|
1173
|
+
ON rank_tracking_configs(project_id, updated_at DESC, id);
|
|
1174
|
+
CREATE INDEX IF NOT EXISTS idx_rank_tracking_configs_due
|
|
1175
|
+
ON rank_tracking_configs(cadence, next_run_at, id);
|
|
1176
|
+
|
|
1177
|
+
CREATE TABLE IF NOT EXISTS rank_tracking_runs (
|
|
1178
|
+
id TEXT PRIMARY KEY,
|
|
1179
|
+
config_id TEXT NOT NULL,
|
|
1180
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
1181
|
+
state TEXT NOT NULL CHECK(state IN ('pending', 'partial', 'failed', 'complete')),
|
|
1182
|
+
collection_method TEXT NOT NULL CHECK(collection_method IN ('live', 'queued')),
|
|
1183
|
+
scheduled_for INTEGER NOT NULL,
|
|
1184
|
+
started_at INTEGER NOT NULL,
|
|
1185
|
+
completed_at INTEGER,
|
|
1186
|
+
keyword_count INTEGER NOT NULL,
|
|
1187
|
+
task_count INTEGER NOT NULL,
|
|
1188
|
+
snapshot_count INTEGER NOT NULL DEFAULT 0,
|
|
1189
|
+
pending_count INTEGER NOT NULL,
|
|
1190
|
+
failed_count INTEGER NOT NULL DEFAULT 0,
|
|
1191
|
+
estimated_cost_micros INTEGER,
|
|
1192
|
+
actual_cost_micros INTEGER,
|
|
1193
|
+
config_snapshot_json TEXT NOT NULL,
|
|
1194
|
+
error_summary TEXT,
|
|
1195
|
+
FOREIGN KEY(config_id) REFERENCES rank_tracking_configs(id) ON DELETE CASCADE
|
|
1196
|
+
);
|
|
1197
|
+
CREATE INDEX IF NOT EXISTS idx_rank_tracking_runs_config
|
|
1198
|
+
ON rank_tracking_runs(config_id, started_at DESC, id DESC);
|
|
1199
|
+
DROP INDEX IF EXISTS idx_rank_tracking_active_run;
|
|
1200
|
+
CREATE UNIQUE INDEX idx_rank_tracking_active_run
|
|
1201
|
+
ON rank_tracking_runs(config_id)
|
|
1202
|
+
WHERE completed_at IS NULL;
|
|
1203
|
+
|
|
1204
|
+
CREATE TABLE IF NOT EXISTS rank_tracking_tasks (
|
|
1205
|
+
id TEXT PRIMARY KEY,
|
|
1206
|
+
run_id TEXT NOT NULL,
|
|
1207
|
+
normalized_keyword TEXT NOT NULL,
|
|
1208
|
+
display_keyword TEXT NOT NULL,
|
|
1209
|
+
device TEXT NOT NULL CHECK(device IN ('desktop', 'mobile')),
|
|
1210
|
+
state TEXT NOT NULL CHECK(state IN ('pending', 'posting', 'posted', 'complete', 'failed')),
|
|
1211
|
+
provider_task_id TEXT,
|
|
1212
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
1213
|
+
posted_at INTEGER,
|
|
1214
|
+
collected_at INTEGER,
|
|
1215
|
+
error_code TEXT,
|
|
1216
|
+
error_message TEXT,
|
|
1217
|
+
UNIQUE(run_id, normalized_keyword, device),
|
|
1218
|
+
UNIQUE(provider_task_id),
|
|
1219
|
+
FOREIGN KEY(run_id) REFERENCES rank_tracking_runs(id) ON DELETE CASCADE
|
|
1220
|
+
);
|
|
1221
|
+
CREATE INDEX IF NOT EXISTS idx_rank_tracking_tasks_run
|
|
1222
|
+
ON rank_tracking_tasks(run_id, state, normalized_keyword, device);
|
|
1223
|
+
CREATE INDEX IF NOT EXISTS idx_rank_tracking_tasks_provider
|
|
1224
|
+
ON rank_tracking_tasks(provider_task_id, state);
|
|
1225
|
+
|
|
1226
|
+
CREATE TABLE IF NOT EXISTS rank_tracking_snapshots (
|
|
1227
|
+
task_id TEXT PRIMARY KEY,
|
|
1228
|
+
run_id TEXT NOT NULL,
|
|
1229
|
+
normalized_keyword TEXT NOT NULL,
|
|
1230
|
+
display_keyword TEXT NOT NULL,
|
|
1231
|
+
device TEXT NOT NULL CHECK(device IN ('desktop', 'mobile')),
|
|
1232
|
+
observation_state TEXT NOT NULL CHECK(observation_state IN ('observed', 'not_observed_within_depth')),
|
|
1233
|
+
organic_position INTEGER,
|
|
1234
|
+
absolute_position INTEGER,
|
|
1235
|
+
ranking_url TEXT,
|
|
1236
|
+
observed_features_json TEXT NOT NULL,
|
|
1237
|
+
checked_at TEXT NOT NULL,
|
|
1238
|
+
provider TEXT NOT NULL CHECK(provider IN ('dataforseo', 'semrush', 'ahrefs')),
|
|
1239
|
+
provider_task_id TEXT,
|
|
1240
|
+
requested_depth INTEGER NOT NULL,
|
|
1241
|
+
returned_rows INTEGER,
|
|
1242
|
+
retained_rows INTEGER,
|
|
1243
|
+
invalid_rows INTEGER NOT NULL,
|
|
1244
|
+
completeness TEXT NOT NULL,
|
|
1245
|
+
estimated_cost_micros INTEGER,
|
|
1246
|
+
actual_cost_micros INTEGER,
|
|
1247
|
+
warnings_json TEXT NOT NULL,
|
|
1248
|
+
created_at INTEGER NOT NULL,
|
|
1249
|
+
FOREIGN KEY(task_id) REFERENCES rank_tracking_tasks(id) ON DELETE CASCADE,
|
|
1250
|
+
FOREIGN KEY(run_id) REFERENCES rank_tracking_runs(id) ON DELETE CASCADE
|
|
1251
|
+
);
|
|
1252
|
+
CREATE INDEX IF NOT EXISTS idx_rank_tracking_snapshots_run
|
|
1253
|
+
ON rank_tracking_snapshots(run_id, normalized_keyword, device);
|
|
1254
|
+
`;
|
|
1255
|
+
//#endregion
|
|
1151
1256
|
//#region packages/core/src/storage/cache-maintenance.ts
|
|
1152
1257
|
const MEBIBYTE$2 = 1024 * 1024;
|
|
1153
1258
|
const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
|
|
@@ -1704,6 +1809,7 @@ function initDb(database, isNewDatabase) {
|
|
|
1704
1809
|
database.transaction(() => {
|
|
1705
1810
|
database.exec(CREATE_SQL);
|
|
1706
1811
|
database.exec(KEYWORD_SET_SCHEMA_SQL);
|
|
1812
|
+
database.exec(RANK_TRACKING_SCHEMA_SQL);
|
|
1707
1813
|
database.exec(PROVIDER_SPEND_SCHEMA_SQL);
|
|
1708
1814
|
database.exec(`DELETE FROM semrush_cache WHERE request_json LIKE '%"key"%'`);
|
|
1709
1815
|
ensureColumn(database, "crawl_pages", "snapshot_json", "TEXT");
|
|
@@ -4937,7 +5043,7 @@ async function fetchPage(url, opts = {}, dependencies = {}) {
|
|
|
4937
5043
|
}
|
|
4938
5044
|
//#endregion
|
|
4939
5045
|
//#region packages/core/src/gsc/property-url.ts
|
|
4940
|
-
function invalid$
|
|
5046
|
+
function invalid$2(message) {
|
|
4941
5047
|
throw new SeoError("INVALID_INPUT", message);
|
|
4942
5048
|
}
|
|
4943
5049
|
function normalizeHttpUrl(value) {
|
|
@@ -4945,10 +5051,10 @@ function normalizeHttpUrl(value) {
|
|
|
4945
5051
|
try {
|
|
4946
5052
|
url = new URL(value.trim());
|
|
4947
5053
|
} catch {
|
|
4948
|
-
return invalid$
|
|
5054
|
+
return invalid$2(`Invalid URL: ${value}`);
|
|
4949
5055
|
}
|
|
4950
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return invalid$
|
|
4951
|
-
if (url.username || url.password) return invalid$
|
|
5056
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return invalid$2("The report URL must use http:// or https://.");
|
|
5057
|
+
if (url.username || url.password) return invalid$2("The report URL must not contain embedded credentials.");
|
|
4952
5058
|
url.hash = "";
|
|
4953
5059
|
return url.toString();
|
|
4954
5060
|
}
|
|
@@ -4957,13 +5063,13 @@ function assertUrlMatchesGscProperty(property, value) {
|
|
|
4957
5063
|
const parsed = new URL(url);
|
|
4958
5064
|
if (property.startsWith("sc-domain:")) {
|
|
4959
5065
|
const domain = property.slice(10).trim().toLowerCase();
|
|
4960
|
-
if (!domain || /[/:]/.test(domain)) return invalid$
|
|
5066
|
+
if (!domain || /[/:]/.test(domain)) return invalid$2(`Invalid Search Console domain property: ${property}`);
|
|
4961
5067
|
const hostname = parsed.hostname.toLowerCase();
|
|
4962
|
-
if (hostname !== domain && !hostname.endsWith(`.${domain}`)) return invalid$
|
|
5068
|
+
if (hostname !== domain && !hostname.endsWith(`.${domain}`)) return invalid$2(`${url} is outside the Search Console property ${property}.`);
|
|
4963
5069
|
return url;
|
|
4964
5070
|
}
|
|
4965
5071
|
const prefix = normalizeHttpUrl(property);
|
|
4966
|
-
if (!url.startsWith(prefix)) return invalid$
|
|
5072
|
+
if (!url.startsWith(prefix)) return invalid$2(`${url} is outside the Search Console property ${property}.`);
|
|
4967
5073
|
return url;
|
|
4968
5074
|
}
|
|
4969
5075
|
//#endregion
|
|
@@ -7170,7 +7276,7 @@ function selectBenchmarkRows$1(input) {
|
|
|
7170
7276
|
function compareText$15(left, right) {
|
|
7171
7277
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
7172
7278
|
}
|
|
7173
|
-
function compareItems$
|
|
7279
|
+
function compareItems$2(left, right) {
|
|
7174
7280
|
return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$15(left.query, right.query) || compareText$15(left.url, right.url);
|
|
7175
7281
|
}
|
|
7176
7282
|
function analyzeRow(input) {
|
|
@@ -7235,7 +7341,7 @@ function analyzePageOpportunitiesFromRows(input) {
|
|
|
7235
7341
|
reportInput: input,
|
|
7236
7342
|
benchmarkContext,
|
|
7237
7343
|
excludedTargetRows
|
|
7238
|
-
})).sort(compareItems$
|
|
7344
|
+
})).sort(compareItems$2).slice(0, limit);
|
|
7239
7345
|
const estimatedCtrClickShortfall = Number(items.reduce((sum, item) => sum + (item.estimatedCtrClickShortfall ?? 0), 0).toFixed(2));
|
|
7240
7346
|
return {
|
|
7241
7347
|
site: input.site,
|
|
@@ -18764,7 +18870,7 @@ function compareRetainedSegmentRows(input) {
|
|
|
18764
18870
|
}
|
|
18765
18871
|
//#endregion
|
|
18766
18872
|
//#region packages/core/src/analyze/segment-impact-analysis.ts
|
|
18767
|
-
function verdict$
|
|
18873
|
+
function verdict$5(input) {
|
|
18768
18874
|
if (input.status === "empty") return "Search Console returned no retained segment rows for either window.";
|
|
18769
18875
|
if (!input.matchedRows) return "No segment was retained in both windows, so no movement delta was inferred.";
|
|
18770
18876
|
const suffix = input.unmatchedRows ? ` ${input.unmatchedRows} one-window rows were kept as unmatched evidence and not converted to zero.` : "";
|
|
@@ -18857,7 +18963,7 @@ function analyzeSegmentRows(input) {
|
|
|
18857
18963
|
matchedRows: matched.length,
|
|
18858
18964
|
returnedRows: selection.returnedRows,
|
|
18859
18965
|
unmatchedRows: unmatched.length,
|
|
18860
|
-
verdict: verdict$
|
|
18966
|
+
verdict: verdict$5({
|
|
18861
18967
|
status: dataStatus,
|
|
18862
18968
|
matchedRows: matched.length,
|
|
18863
18969
|
returnedRows: selection.returnedRows,
|
|
@@ -19670,7 +19776,7 @@ const defaultDependencies$10 = {
|
|
|
19670
19776
|
searchAnalytics: querySearchAnalytics,
|
|
19671
19777
|
now: () => /* @__PURE__ */ new Date()
|
|
19672
19778
|
};
|
|
19673
|
-
function verdict$
|
|
19779
|
+
function verdict$4(input) {
|
|
19674
19780
|
if (input.status === "empty") return "Search Console returned no retained query/page rows for this window.";
|
|
19675
19781
|
if (!input.eligible) {
|
|
19676
19782
|
if (input.status === "partial") return "No material multi-URL query candidates remained in the validated retained rows, but partial evidence prevents an all-clear.";
|
|
@@ -19797,7 +19903,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
19797
19903
|
returnedClusters: analysis.selection.returnedClusters,
|
|
19798
19904
|
suppressedQueries: analysis.selection.suppressedQueries,
|
|
19799
19905
|
brandFiltering: input.includeBrand ? "included" : "excluded",
|
|
19800
|
-
verdict: verdict$
|
|
19906
|
+
verdict: verdict$4({
|
|
19801
19907
|
status: dataStatus,
|
|
19802
19908
|
eligible: analysis.selection.eligibleClusters,
|
|
19803
19909
|
returned: analysis.selection.returnedClusters
|
|
@@ -20083,7 +20189,7 @@ const defaultDependencies$9 = {
|
|
|
20083
20189
|
searchAnalytics: querySearchAnalytics,
|
|
20084
20190
|
now: () => /* @__PURE__ */ new Date()
|
|
20085
20191
|
};
|
|
20086
|
-
function verdict$
|
|
20192
|
+
function verdict$3(report) {
|
|
20087
20193
|
if (report.dataStatus === "empty") return "Search Console returned no retained query/page rows for either comparison window.";
|
|
20088
20194
|
if (report.dataStatus === "partial" && report.selection.currentAggregatedRows === 0) return "The current retained-row window is empty, so no query/page losses were inferred.";
|
|
20089
20195
|
if (report.dataStatus === "partial" && report.selection.previousAggregatedRows === 0) return "The comparison retained-row window is empty, so query/page decay could not be measured.";
|
|
@@ -20228,7 +20334,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
20228
20334
|
observedRetainedQueryClickLoss: analysis.totals.eligibleObservedRetainedQueryClickLoss,
|
|
20229
20335
|
returnedObservedRetainedQueryClickLoss: analysis.totals.returnedObservedRetainedQueryClickLoss,
|
|
20230
20336
|
brandFiltering: input.includeBrand ? "included" : "excluded",
|
|
20231
|
-
verdict: verdict$
|
|
20337
|
+
verdict: verdict$3(reportBase)
|
|
20232
20338
|
},
|
|
20233
20339
|
caveats: [
|
|
20234
20340
|
`Current window: ${currentRange.startDate} to ${currentRange.endDate}; comparison: ${previousRange.startDate} to ${previousRange.endDate}.`,
|
|
@@ -21022,7 +21128,7 @@ function templatesFromGroups(groups) {
|
|
|
21022
21128
|
sampleUrls: group.sampleUrls
|
|
21023
21129
|
}));
|
|
21024
21130
|
}
|
|
21025
|
-
function verdict$
|
|
21131
|
+
function verdict$2(analysis) {
|
|
21026
21132
|
if (analysis.dataStatus === "empty") return "Search Console returned no retained query/page rows for this date window.";
|
|
21027
21133
|
if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
|
|
21028
21134
|
return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
|
|
@@ -21106,7 +21212,7 @@ async function strikingDistance(input, dependencies = defaultDependencies$7) {
|
|
|
21106
21212
|
summary: {
|
|
21107
21213
|
...analysis.summary,
|
|
21108
21214
|
brandFiltering: input.includeBrand ? "included" : "excluded",
|
|
21109
|
-
verdict: verdict$
|
|
21215
|
+
verdict: verdict$2(analysis)
|
|
21110
21216
|
},
|
|
21111
21217
|
caveats: [
|
|
21112
21218
|
`Date window: ${range.startDate} to ${range.endDate}, using final GSC data where available.`,
|
|
@@ -24292,7 +24398,10 @@ const userDataPriceSchema = z.object({
|
|
|
24292
24398
|
keyword_suggestions: livePriceSchema.optional(),
|
|
24293
24399
|
related_keywords: livePriceSchema.optional()
|
|
24294
24400
|
}).passthrough().optional(),
|
|
24295
|
-
serp: z.object({
|
|
24401
|
+
serp: z.object({
|
|
24402
|
+
task_post: normalPriorityPriceSchema.optional(),
|
|
24403
|
+
live: z.object({ advanced: normalPriorityPriceSchema.optional() }).passthrough().optional()
|
|
24404
|
+
}).passthrough().optional()
|
|
24296
24405
|
}).passthrough();
|
|
24297
24406
|
const userDataResultSchema = z.object({
|
|
24298
24407
|
login: z.string().min(1).max(320),
|
|
@@ -24410,19 +24519,19 @@ const dataForSeoDiscoveryResponseSchema = z.object({
|
|
|
24410
24519
|
}).passthrough();
|
|
24411
24520
|
//#endregion
|
|
24412
24521
|
//#region packages/core/src/providers/dataforseo/paid-request.ts
|
|
24413
|
-
function usdToMicros
|
|
24522
|
+
function usdToMicros(value) {
|
|
24414
24523
|
if (value === void 0) return null;
|
|
24415
24524
|
return Math.round(value * 1e6);
|
|
24416
24525
|
}
|
|
24417
24526
|
function responseTaskIds$1(response) {
|
|
24418
24527
|
return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
24419
24528
|
}
|
|
24420
|
-
function responseCostMicros
|
|
24421
|
-
if (response.cost !== void 0) return usdToMicros
|
|
24529
|
+
function responseCostMicros(response) {
|
|
24530
|
+
if (response.cost !== void 0) return usdToMicros(response.cost);
|
|
24422
24531
|
if (response.tasks.some((task) => task.cost === void 0)) return null;
|
|
24423
|
-
return response.tasks.reduce((sum, task) => sum + (usdToMicros
|
|
24532
|
+
return response.tasks.reduce((sum, task) => sum + (usdToMicros(task.cost) ?? 0), 0);
|
|
24424
24533
|
}
|
|
24425
|
-
function taskErrorCode
|
|
24534
|
+
function taskErrorCode(statusCode) {
|
|
24426
24535
|
if (statusCode >= 40100 && statusCode < 40200) return "authentication";
|
|
24427
24536
|
if (statusCode === 40202) return "rate-limit";
|
|
24428
24537
|
return "remote-error";
|
|
@@ -24512,7 +24621,7 @@ async function dataForSeoPaidPost(input) {
|
|
|
24512
24621
|
});
|
|
24513
24622
|
throw error;
|
|
24514
24623
|
}
|
|
24515
|
-
const actualCostMicros = responseCostMicros
|
|
24624
|
+
const actualCostMicros = responseCostMicros(response);
|
|
24516
24625
|
const returnedRows = input.rowCount(response);
|
|
24517
24626
|
const taskIds = responseTaskIds$1(response);
|
|
24518
24627
|
const failedTasks = response.tasks.filter((task) => task.status_code !== 2e4);
|
|
@@ -24534,7 +24643,7 @@ async function dataForSeoPaidPost(input) {
|
|
|
24534
24643
|
throw new ProviderError({
|
|
24535
24644
|
provider: "dataforseo",
|
|
24536
24645
|
operation: input.operation,
|
|
24537
|
-
code: taskErrorCode
|
|
24646
|
+
code: taskErrorCode(statusCode),
|
|
24538
24647
|
message: `DataForSEO could not complete ${input.operation} (${statusCode}).`,
|
|
24539
24648
|
retryable: statusCode === 40202
|
|
24540
24649
|
});
|
|
@@ -24577,6 +24686,22 @@ async function dataForSeoPaidPost(input) {
|
|
|
24577
24686
|
};
|
|
24578
24687
|
}
|
|
24579
24688
|
//#endregion
|
|
24689
|
+
//#region packages/core/src/providers/dataforseo/response.ts
|
|
24690
|
+
function dataForSeoUsdToMicros(value) {
|
|
24691
|
+
if (value === void 0) return null;
|
|
24692
|
+
return Math.round(value * 1e6);
|
|
24693
|
+
}
|
|
24694
|
+
function dataForSeoTaskErrorCode(statusCode) {
|
|
24695
|
+
if (statusCode >= 40100 && statusCode < 40200) return "authentication";
|
|
24696
|
+
if (statusCode === 40202) return "rate-limit";
|
|
24697
|
+
return "remote-error";
|
|
24698
|
+
}
|
|
24699
|
+
function dataForSeoResponseCostMicros(response) {
|
|
24700
|
+
if (response.cost !== void 0) return dataForSeoUsdToMicros(response.cost);
|
|
24701
|
+
if (response.tasks.some((task) => task.cost === void 0)) return null;
|
|
24702
|
+
return response.tasks.reduce((sum, task) => sum + (dataForSeoUsdToMicros(task.cost) ?? 0), 0);
|
|
24703
|
+
}
|
|
24704
|
+
//#endregion
|
|
24580
24705
|
//#region packages/core/src/providers/dataforseo/serp-schema.ts
|
|
24581
24706
|
const serpItemSchema = z.object({
|
|
24582
24707
|
type: z.string().trim().min(1).max(100),
|
|
@@ -24621,6 +24746,237 @@ const dataForSeoSerpResponseSchema = z.object({
|
|
|
24621
24746
|
tasks: z.array(serpTaskSchema).max(10)
|
|
24622
24747
|
}).passthrough();
|
|
24623
24748
|
//#endregion
|
|
24749
|
+
//#region packages/core/src/providers/dataforseo/serp-task-schema.ts
|
|
24750
|
+
const taskPostItemSchema = z.object({
|
|
24751
|
+
id: z.string().trim().min(1).max(100).optional(),
|
|
24752
|
+
status_code: z.number().int(),
|
|
24753
|
+
status_message: z.string().max(1e3),
|
|
24754
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
24755
|
+
result_count: z.number().int().nonnegative().optional(),
|
|
24756
|
+
data: z.object({ tag: z.string().trim().min(1).max(255).optional() }).passthrough().optional()
|
|
24757
|
+
}).passthrough();
|
|
24758
|
+
const dataForSeoSerpTaskPostResponseSchema = z.object({
|
|
24759
|
+
status_code: z.number().int(),
|
|
24760
|
+
status_message: z.string().max(1e3),
|
|
24761
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
24762
|
+
tasks_count: z.number().int().nonnegative(),
|
|
24763
|
+
tasks_error: z.number().int().nonnegative(),
|
|
24764
|
+
tasks: z.array(taskPostItemSchema).max(100)
|
|
24765
|
+
}).passthrough();
|
|
24766
|
+
const readyResultSchema = z.object({
|
|
24767
|
+
id: z.string().trim().min(1).max(100),
|
|
24768
|
+
tag: z.string().trim().max(255).nullable().optional(),
|
|
24769
|
+
se: z.string().trim().max(100).optional(),
|
|
24770
|
+
se_type: z.string().trim().max(100).optional(),
|
|
24771
|
+
endpoint_advanced: z.string().trim().max(2048).nullable().optional()
|
|
24772
|
+
}).passthrough();
|
|
24773
|
+
const readyTaskSchema = z.object({
|
|
24774
|
+
id: z.string().trim().min(1).max(100).optional(),
|
|
24775
|
+
status_code: z.number().int(),
|
|
24776
|
+
status_message: z.string().max(1e3),
|
|
24777
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
24778
|
+
result: z.array(readyResultSchema).max(1e3).nullable().optional()
|
|
24779
|
+
}).passthrough();
|
|
24780
|
+
const dataForSeoSerpTasksReadyResponseSchema = z.object({
|
|
24781
|
+
status_code: z.number().int(),
|
|
24782
|
+
status_message: z.string().max(1e3),
|
|
24783
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
24784
|
+
tasks_count: z.number().int().nonnegative(),
|
|
24785
|
+
tasks_error: z.number().int().nonnegative(),
|
|
24786
|
+
tasks: z.array(readyTaskSchema).max(10)
|
|
24787
|
+
}).passthrough();
|
|
24788
|
+
//#endregion
|
|
24789
|
+
//#region packages/core/src/providers/dataforseo/serp-tasks.ts
|
|
24790
|
+
const SERP_TASK_POST_PATH = "v3/serp/google/organic/task_post";
|
|
24791
|
+
const SERP_TASKS_READY_PATH = "v3/serp/google/organic/tasks_ready";
|
|
24792
|
+
const SERP_TASK_GET_ADVANCED_PATH = "v3/serp/google/organic/task_get/advanced/";
|
|
24793
|
+
function dataForSeoSerpRows(response) {
|
|
24794
|
+
return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
|
|
24795
|
+
}
|
|
24796
|
+
function validateDataForSeoSerpTaskId(providerTaskId) {
|
|
24797
|
+
if (!/^[a-zA-Z0-9-]{1,100}$/u.test(providerTaskId)) throw new ProviderError({
|
|
24798
|
+
provider: "dataforseo",
|
|
24799
|
+
operation: "serp-task-get",
|
|
24800
|
+
code: "configuration",
|
|
24801
|
+
message: "Use a valid queued SERP task id."
|
|
24802
|
+
});
|
|
24803
|
+
}
|
|
24804
|
+
async function postDataForSeoSerpTasks(input, transport) {
|
|
24805
|
+
const { price, requests } = input;
|
|
24806
|
+
if (price.perRequestMicros === null || price.perResultMicros === null) throw new ProviderError({
|
|
24807
|
+
provider: "dataforseo",
|
|
24808
|
+
operation: "serp-task-post",
|
|
24809
|
+
code: "invalid-response",
|
|
24810
|
+
message: "DataForSEO Standard SERP pricing is unavailable, so queued tasks were not posted."
|
|
24811
|
+
});
|
|
24812
|
+
const requestUnits = requests.reduce((total, request) => total + Math.ceil(request.depth / 10), 0);
|
|
24813
|
+
const estimatedCostMicros = price.perRequestMicros * requestUnits + price.perResultMicros * requests.length;
|
|
24814
|
+
const reservation = reserveProviderSpend({
|
|
24815
|
+
provider: "dataforseo",
|
|
24816
|
+
capability: "serp-snapshot",
|
|
24817
|
+
endpoint: SERP_TASK_POST_PATH,
|
|
24818
|
+
projectId: input.context.projectId,
|
|
24819
|
+
reportId: input.context.reportId,
|
|
24820
|
+
reportRunId: input.context.reportRunId,
|
|
24821
|
+
requestedRows: requests.length,
|
|
24822
|
+
estimatedCostMicros
|
|
24823
|
+
}, {
|
|
24824
|
+
database: transport.database,
|
|
24825
|
+
limits: transport.spendLimits,
|
|
24826
|
+
now: transport.now().getTime()
|
|
24827
|
+
});
|
|
24828
|
+
let response;
|
|
24829
|
+
try {
|
|
24830
|
+
response = await providerRequestJson({
|
|
24831
|
+
provider: "dataforseo",
|
|
24832
|
+
operation: "serp-task-post",
|
|
24833
|
+
url: new URL(SERP_TASK_POST_PATH, transport.baseUrl),
|
|
24834
|
+
fetch: transport.fetch,
|
|
24835
|
+
maxResponseBytes: transport.maxResponseBytes,
|
|
24836
|
+
timeoutMs: transport.timeoutMs,
|
|
24837
|
+
retry: "never",
|
|
24838
|
+
schema: dataForSeoSerpTaskPostResponseSchema,
|
|
24839
|
+
init: {
|
|
24840
|
+
method: "POST",
|
|
24841
|
+
headers: {
|
|
24842
|
+
authorization: transport.authorization,
|
|
24843
|
+
"content-type": "application/json"
|
|
24844
|
+
},
|
|
24845
|
+
body: JSON.stringify(requests)
|
|
24846
|
+
}
|
|
24847
|
+
});
|
|
24848
|
+
} catch (error) {
|
|
24849
|
+
finalizeProviderSpend(reservation.id, {
|
|
24850
|
+
provider: "dataforseo",
|
|
24851
|
+
state: "failed",
|
|
24852
|
+
actualCostMicros: null,
|
|
24853
|
+
returnedRows: null,
|
|
24854
|
+
taskIds: []
|
|
24855
|
+
}, {
|
|
24856
|
+
database: transport.database,
|
|
24857
|
+
limits: transport.spendLimits,
|
|
24858
|
+
now: transport.now().getTime()
|
|
24859
|
+
});
|
|
24860
|
+
throw error;
|
|
24861
|
+
}
|
|
24862
|
+
const taskReceipts = response.tasks.flatMap((task) => task.id && task.data?.tag ? [{
|
|
24863
|
+
providerTaskId: task.id,
|
|
24864
|
+
tag: task.data.tag
|
|
24865
|
+
}] : []);
|
|
24866
|
+
const taskIds = taskReceipts.map((receipt) => receipt.providerTaskId);
|
|
24867
|
+
const expectedTags = new Set(requests.map((request) => request.tag));
|
|
24868
|
+
const returnedTags = new Set(taskReceipts.map((receipt) => receipt.tag));
|
|
24869
|
+
const receiptsMatch = expectedTags.size === requests.length && returnedTags.size === expectedTags.size && [...expectedTags].every((tag) => returnedTags.has(tag));
|
|
24870
|
+
const failedTasks = response.tasks.filter((task) => task.status_code !== 20100 || !task.id || !task.data?.tag);
|
|
24871
|
+
const actualCostMicros = dataForSeoResponseCostMicros(response);
|
|
24872
|
+
const state = failedTasks.length === 0 && response.tasks_error === 0 ? "succeeded" : failedTasks.length < response.tasks.length ? "partial" : "failed";
|
|
24873
|
+
const spendNotice = finalizeProviderSpend(reservation.id, {
|
|
24874
|
+
provider: "dataforseo",
|
|
24875
|
+
state,
|
|
24876
|
+
actualCostMicros,
|
|
24877
|
+
returnedRows: taskIds.length,
|
|
24878
|
+
taskIds
|
|
24879
|
+
}, {
|
|
24880
|
+
database: transport.database,
|
|
24881
|
+
limits: transport.spendLimits,
|
|
24882
|
+
now: transport.now().getTime()
|
|
24883
|
+
});
|
|
24884
|
+
if (response.status_code !== 2e4 || failedTasks.length > 0 || taskIds.length !== requests.length || !receiptsMatch) {
|
|
24885
|
+
const statusCode = failedTasks[0]?.status_code ?? response.status_code;
|
|
24886
|
+
throw new ProviderError({
|
|
24887
|
+
provider: "dataforseo",
|
|
24888
|
+
operation: "serp-task-post",
|
|
24889
|
+
code: dataForSeoTaskErrorCode(statusCode),
|
|
24890
|
+
message: `DataForSEO accepted ${taskIds.length} of ${requests.length} queued SERP tasks (${statusCode}).`
|
|
24891
|
+
});
|
|
24892
|
+
}
|
|
24893
|
+
return {
|
|
24894
|
+
taskIds,
|
|
24895
|
+
taskReceipts,
|
|
24896
|
+
estimatedCostMicros,
|
|
24897
|
+
actualCostMicros,
|
|
24898
|
+
spendNotice,
|
|
24899
|
+
warnings: [],
|
|
24900
|
+
observedAt: transport.now().toISOString()
|
|
24901
|
+
};
|
|
24902
|
+
}
|
|
24903
|
+
async function listReadyDataForSeoSerpTasks(transport) {
|
|
24904
|
+
const response = await providerRequestJson({
|
|
24905
|
+
provider: "dataforseo",
|
|
24906
|
+
operation: "serp-tasks-ready",
|
|
24907
|
+
url: new URL(SERP_TASKS_READY_PATH, transport.baseUrl),
|
|
24908
|
+
fetch: transport.fetch,
|
|
24909
|
+
maxResponseBytes: transport.maxResponseBytes,
|
|
24910
|
+
timeoutMs: transport.timeoutMs,
|
|
24911
|
+
retry: "safe",
|
|
24912
|
+
schema: dataForSeoSerpTasksReadyResponseSchema,
|
|
24913
|
+
init: {
|
|
24914
|
+
method: "GET",
|
|
24915
|
+
headers: { authorization: transport.authorization }
|
|
24916
|
+
}
|
|
24917
|
+
});
|
|
24918
|
+
const failedTask = response.tasks.find((task) => task.status_code !== 2e4);
|
|
24919
|
+
if (response.status_code !== 2e4 || failedTask) {
|
|
24920
|
+
const statusCode = failedTask?.status_code ?? response.status_code;
|
|
24921
|
+
throw new ProviderError({
|
|
24922
|
+
provider: "dataforseo",
|
|
24923
|
+
operation: "serp-tasks-ready",
|
|
24924
|
+
code: dataForSeoTaskErrorCode(statusCode),
|
|
24925
|
+
message: `DataForSEO could not list ready SERP tasks (${statusCode}).`,
|
|
24926
|
+
retryable: statusCode === 40202
|
|
24927
|
+
});
|
|
24928
|
+
}
|
|
24929
|
+
return response.tasks.flatMap((task) => (task.result ?? []).map((result) => ({
|
|
24930
|
+
providerTaskId: result.id,
|
|
24931
|
+
tag: result.tag ?? null
|
|
24932
|
+
})));
|
|
24933
|
+
}
|
|
24934
|
+
async function getDataForSeoSerpTask(providerTaskId, transport) {
|
|
24935
|
+
const response = await providerRequestJson({
|
|
24936
|
+
provider: "dataforseo",
|
|
24937
|
+
operation: "serp-task-get",
|
|
24938
|
+
url: new URL(`${SERP_TASK_GET_ADVANCED_PATH}${providerTaskId}`, transport.baseUrl),
|
|
24939
|
+
fetch: transport.fetch,
|
|
24940
|
+
maxResponseBytes: transport.maxResponseBytes,
|
|
24941
|
+
timeoutMs: transport.timeoutMs,
|
|
24942
|
+
retry: "safe",
|
|
24943
|
+
schema: dataForSeoSerpResponseSchema,
|
|
24944
|
+
init: {
|
|
24945
|
+
method: "GET",
|
|
24946
|
+
headers: { authorization: transport.authorization }
|
|
24947
|
+
}
|
|
24948
|
+
});
|
|
24949
|
+
const failedTask = response.tasks.find((task) => task.status_code !== 2e4);
|
|
24950
|
+
if (response.status_code !== 2e4 || failedTask) {
|
|
24951
|
+
const statusCode = failedTask?.status_code ?? response.status_code;
|
|
24952
|
+
throw new ProviderError({
|
|
24953
|
+
provider: "dataforseo",
|
|
24954
|
+
operation: "serp-task-get",
|
|
24955
|
+
code: dataForSeoTaskErrorCode(statusCode),
|
|
24956
|
+
message: `DataForSEO could not collect queued SERP task ${providerTaskId} (${statusCode}).`,
|
|
24957
|
+
retryable: statusCode === 40202
|
|
24958
|
+
});
|
|
24959
|
+
}
|
|
24960
|
+
return {
|
|
24961
|
+
response,
|
|
24962
|
+
observedAt: transport.now().toISOString(),
|
|
24963
|
+
returnedRows: dataForSeoSerpRows(response),
|
|
24964
|
+
cache: {
|
|
24965
|
+
status: "bypass",
|
|
24966
|
+
storedAt: null,
|
|
24967
|
+
expiresAt: null
|
|
24968
|
+
},
|
|
24969
|
+
cost: {
|
|
24970
|
+
currency: "USD",
|
|
24971
|
+
estimatedMicros: 0,
|
|
24972
|
+
actualMicros: dataForSeoResponseCostMicros(response) ?? 0,
|
|
24973
|
+
taskIds: [providerTaskId]
|
|
24974
|
+
},
|
|
24975
|
+
spendNotice: null,
|
|
24976
|
+
warnings: []
|
|
24977
|
+
};
|
|
24978
|
+
}
|
|
24979
|
+
//#endregion
|
|
24624
24980
|
//#region packages/core/src/providers/dataforseo/client.ts
|
|
24625
24981
|
const DEFAULT_BASE_URL$1 = "https://api.dataforseo.com/";
|
|
24626
24982
|
const USER_DATA_PATH = "v3/appendix/user_data";
|
|
@@ -24643,9 +24999,43 @@ const MAX_KEYWORD_WORDS = 10;
|
|
|
24643
24999
|
const MAX_DISCOVERY_SEEDS$1 = 5;
|
|
24644
25000
|
const MAX_DISCOVERY_ROWS$1 = 100;
|
|
24645
25001
|
const MAX_SERP_DEPTH$1 = 100;
|
|
24646
|
-
function
|
|
24647
|
-
|
|
24648
|
-
|
|
25002
|
+
function validateSerpInput(input) {
|
|
25003
|
+
const keyword = input.keyword.trim().replace(/\s+/gu, " ");
|
|
25004
|
+
if (keyword.length < 1 || keyword.length > MAX_KEYWORD_CHARACTERS || keyword.split(/\s+/u).length > MAX_KEYWORD_WORDS) throw new ProviderError({
|
|
25005
|
+
provider: "dataforseo",
|
|
25006
|
+
operation: "serp-snapshot",
|
|
25007
|
+
code: "configuration",
|
|
25008
|
+
message: `SERP snapshots require a keyword of at most ${MAX_KEYWORD_CHARACTERS} characters and ${MAX_KEYWORD_WORDS} words.`
|
|
25009
|
+
});
|
|
25010
|
+
if (/(?:^|\s)(?:allinanchor|allintext|allintitle|allinurl|cache|define|definition|filetype|id|inanchor|info|intext|intitle|inurl|link|site):/iu.test(keyword)) throw new ProviderError({
|
|
25011
|
+
provider: "dataforseo",
|
|
25012
|
+
operation: "serp-snapshot",
|
|
25013
|
+
code: "configuration",
|
|
25014
|
+
message: "SERP snapshot keywords cannot contain search operators with multiplied provider pricing."
|
|
25015
|
+
});
|
|
25016
|
+
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH$1) throw new ProviderError({
|
|
25017
|
+
provider: "dataforseo",
|
|
25018
|
+
operation: "serp-snapshot",
|
|
25019
|
+
code: "configuration",
|
|
25020
|
+
message: `SERP depth must be from 1 to ${MAX_SERP_DEPTH$1}.`
|
|
25021
|
+
});
|
|
25022
|
+
if (!/^[a-z]{2}$/.test(input.languageCode)) throw new ProviderError({
|
|
25023
|
+
provider: "dataforseo",
|
|
25024
|
+
operation: "serp-snapshot",
|
|
25025
|
+
code: "configuration",
|
|
25026
|
+
message: "DataForSEO language code must contain two lowercase letters."
|
|
25027
|
+
});
|
|
25028
|
+
const locationName = input.locationName?.trim();
|
|
25029
|
+
if (input.locationCode !== void 0 === Boolean(locationName)) throw new ProviderError({
|
|
25030
|
+
provider: "dataforseo",
|
|
25031
|
+
operation: "serp-snapshot",
|
|
25032
|
+
code: "configuration",
|
|
25033
|
+
message: "DataForSEO SERP snapshots require exactly one location code or location name."
|
|
25034
|
+
});
|
|
25035
|
+
return {
|
|
25036
|
+
keyword,
|
|
25037
|
+
locationName
|
|
25038
|
+
};
|
|
24649
25039
|
}
|
|
24650
25040
|
function unitPrice(components) {
|
|
24651
25041
|
const prices = components ?? [];
|
|
@@ -24671,19 +25061,12 @@ function keywordDiscoveryPrices(account) {
|
|
|
24671
25061
|
function serpLiveAdvancedPrice(account) {
|
|
24672
25062
|
return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
|
|
24673
25063
|
}
|
|
24674
|
-
function
|
|
24675
|
-
|
|
24676
|
-
if (statusCode === 40202) return "rate-limit";
|
|
24677
|
-
return "remote-error";
|
|
25064
|
+
function serpTaskPostPrice(account) {
|
|
25065
|
+
return unitPrice(account.price?.serp?.task_post?.priority_normal);
|
|
24678
25066
|
}
|
|
24679
25067
|
function responseTaskIds(response) {
|
|
24680
25068
|
return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
24681
25069
|
}
|
|
24682
|
-
function responseCostMicros(response) {
|
|
24683
|
-
if (response.cost !== void 0) return usdToMicros(response.cost);
|
|
24684
|
-
if (response.tasks.some((task) => task.cost === void 0)) return null;
|
|
24685
|
-
return response.tasks.reduce((sum, task) => sum + (usdToMicros(task.cost) ?? 0), 0);
|
|
24686
|
-
}
|
|
24687
25070
|
function responseRows$1(response) {
|
|
24688
25071
|
return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
|
|
24689
25072
|
}
|
|
@@ -24698,9 +25081,6 @@ function discoveryNextCursor(response) {
|
|
|
24698
25081
|
const cursors = response.tasks.flatMap((task) => (task.result ?? []).flatMap((result) => result.offset_token ? [result.offset_token] : []));
|
|
24699
25082
|
return cursors.length === 1 ? cursors[0] ?? null : null;
|
|
24700
25083
|
}
|
|
24701
|
-
function serpRows(response) {
|
|
24702
|
-
return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
|
|
24703
|
-
}
|
|
24704
25084
|
var DataForSeoClient = class {
|
|
24705
25085
|
fetch;
|
|
24706
25086
|
credentials;
|
|
@@ -24743,6 +25123,18 @@ var DataForSeoClient = class {
|
|
|
24743
25123
|
authorization(credentials) {
|
|
24744
25124
|
return `Basic ${Buffer.from(`${credentials.login}:${credentials.password}`).toString("base64")}`;
|
|
24745
25125
|
}
|
|
25126
|
+
serpTaskTransport(credentials) {
|
|
25127
|
+
return {
|
|
25128
|
+
authorization: this.authorization(credentials),
|
|
25129
|
+
baseUrl: this.baseUrl,
|
|
25130
|
+
fetch: this.fetch,
|
|
25131
|
+
maxResponseBytes: this.maxResponseBytes,
|
|
25132
|
+
timeoutMs: this.timeoutMs,
|
|
25133
|
+
now: this.now,
|
|
25134
|
+
database: this.database,
|
|
25135
|
+
spendLimits: this.spendLimits
|
|
25136
|
+
};
|
|
25137
|
+
}
|
|
24746
25138
|
async userDataForCredentials(credentials) {
|
|
24747
25139
|
let response;
|
|
24748
25140
|
try {
|
|
@@ -24777,7 +25169,7 @@ var DataForSeoClient = class {
|
|
|
24777
25169
|
throw new ProviderError({
|
|
24778
25170
|
provider: "dataforseo",
|
|
24779
25171
|
operation: "account-status",
|
|
24780
|
-
code:
|
|
25172
|
+
code: dataForSeoTaskErrorCode(statusCode),
|
|
24781
25173
|
message: `DataForSEO could not return account status (${statusCode}).`,
|
|
24782
25174
|
retryable: statusCode === 40202
|
|
24783
25175
|
});
|
|
@@ -24793,18 +25185,19 @@ var DataForSeoClient = class {
|
|
|
24793
25185
|
provider: "dataforseo",
|
|
24794
25186
|
login: account.login,
|
|
24795
25187
|
timezone: account.timezone ?? null,
|
|
24796
|
-
balanceMicros:
|
|
24797
|
-
depositedMicros:
|
|
24798
|
-
accountDailySpendMicros:
|
|
25188
|
+
balanceMicros: dataForSeoUsdToMicros(account.money?.balance),
|
|
25189
|
+
depositedMicros: dataForSeoUsdToMicros(account.money?.total),
|
|
25190
|
+
accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
|
|
24799
25191
|
accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
|
|
24800
|
-
accountDailyLimitMicros:
|
|
25192
|
+
accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
|
|
24801
25193
|
keywordOverviewPrice: keywordOverviewPrice(account),
|
|
24802
25194
|
keywordDiscoveryPrices: keywordDiscoveryPrices(account),
|
|
24803
25195
|
serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
|
|
25196
|
+
serpTaskPostPrice: serpTaskPostPrice(account),
|
|
24804
25197
|
backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
|
|
24805
25198
|
aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
|
|
24806
25199
|
apiVersion: response.version ?? null,
|
|
24807
|
-
requestCostMicros:
|
|
25200
|
+
requestCostMicros: dataForSeoUsdToMicros(response.cost) ?? 0,
|
|
24808
25201
|
taskIds: response.tasks.flatMap((task) => task.id ? [task.id] : []),
|
|
24809
25202
|
observedAt: this.now().toISOString()
|
|
24810
25203
|
};
|
|
@@ -24980,7 +25373,7 @@ var DataForSeoClient = class {
|
|
|
24980
25373
|
});
|
|
24981
25374
|
throw error;
|
|
24982
25375
|
}
|
|
24983
|
-
const actualCostMicros =
|
|
25376
|
+
const actualCostMicros = dataForSeoResponseCostMicros(response);
|
|
24984
25377
|
const returnedRows = responseRows$1(response);
|
|
24985
25378
|
const taskIds = responseTaskIds(response);
|
|
24986
25379
|
const failedTasks = response.tasks.filter((task) => task.status_code !== 2e4);
|
|
@@ -25002,7 +25395,7 @@ var DataForSeoClient = class {
|
|
|
25002
25395
|
throw new ProviderError({
|
|
25003
25396
|
provider: "dataforseo",
|
|
25004
25397
|
operation: "keyword-metrics",
|
|
25005
|
-
code:
|
|
25398
|
+
code: dataForSeoTaskErrorCode(statusCode),
|
|
25006
25399
|
message: `DataForSEO could not complete the keyword metrics task (${statusCode}).`,
|
|
25007
25400
|
retryable: statusCode === 40202
|
|
25008
25401
|
});
|
|
@@ -25114,38 +25507,7 @@ var DataForSeoClient = class {
|
|
|
25114
25507
|
};
|
|
25115
25508
|
}
|
|
25116
25509
|
async serpLive(input) {
|
|
25117
|
-
const keyword = input
|
|
25118
|
-
if (keyword.length < 1 || keyword.length > MAX_KEYWORD_CHARACTERS || keyword.split(/\s+/u).length > MAX_KEYWORD_WORDS) throw new ProviderError({
|
|
25119
|
-
provider: "dataforseo",
|
|
25120
|
-
operation: "serp-snapshot",
|
|
25121
|
-
code: "configuration",
|
|
25122
|
-
message: `SERP snapshots require a keyword of at most ${MAX_KEYWORD_CHARACTERS} characters and ${MAX_KEYWORD_WORDS} words.`
|
|
25123
|
-
});
|
|
25124
|
-
if (/(?:^|\s)(?:allinanchor|allintext|allintitle|allinurl|cache|define|definition|filetype|id|inanchor|info|intext|intitle|inurl|link|site):/iu.test(keyword)) throw new ProviderError({
|
|
25125
|
-
provider: "dataforseo",
|
|
25126
|
-
operation: "serp-snapshot",
|
|
25127
|
-
code: "configuration",
|
|
25128
|
-
message: "SERP snapshot keywords cannot contain search operators with multiplied provider pricing."
|
|
25129
|
-
});
|
|
25130
|
-
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH$1) throw new ProviderError({
|
|
25131
|
-
provider: "dataforseo",
|
|
25132
|
-
operation: "serp-snapshot",
|
|
25133
|
-
code: "configuration",
|
|
25134
|
-
message: `SERP depth must be from 1 to ${MAX_SERP_DEPTH$1}.`
|
|
25135
|
-
});
|
|
25136
|
-
if (!/^[a-z]{2}$/.test(input.languageCode)) throw new ProviderError({
|
|
25137
|
-
provider: "dataforseo",
|
|
25138
|
-
operation: "serp-snapshot",
|
|
25139
|
-
code: "configuration",
|
|
25140
|
-
message: "DataForSEO language code must contain two lowercase letters."
|
|
25141
|
-
});
|
|
25142
|
-
const locationName = input.locationName?.trim();
|
|
25143
|
-
if (input.locationCode !== void 0 === Boolean(locationName)) throw new ProviderError({
|
|
25144
|
-
provider: "dataforseo",
|
|
25145
|
-
operation: "serp-snapshot",
|
|
25146
|
-
code: "configuration",
|
|
25147
|
-
message: "DataForSEO SERP snapshots require exactly one location code or location name."
|
|
25148
|
-
});
|
|
25510
|
+
const { keyword, locationName } = validateSerpInput(input);
|
|
25149
25511
|
const request = {
|
|
25150
25512
|
keyword,
|
|
25151
25513
|
language_code: input.languageCode,
|
|
@@ -25166,9 +25528,56 @@ var DataForSeoClient = class {
|
|
|
25166
25528
|
context: input.context,
|
|
25167
25529
|
ttlMs: this.serpTtlMs,
|
|
25168
25530
|
refresh: input.refresh,
|
|
25169
|
-
rowCount:
|
|
25531
|
+
rowCount: dataForSeoSerpRows
|
|
25532
|
+
});
|
|
25533
|
+
}
|
|
25534
|
+
async serpTaskPost(input) {
|
|
25535
|
+
if (input.tasks.length < 1 || input.tasks.length > 100) throw new ProviderError({
|
|
25536
|
+
provider: "dataforseo",
|
|
25537
|
+
operation: "serp-task-post",
|
|
25538
|
+
code: "configuration",
|
|
25539
|
+
message: `A queued SERP post requires 1 to 100 tasks.`
|
|
25540
|
+
});
|
|
25541
|
+
const requests = input.tasks.map((task) => {
|
|
25542
|
+
const validated = validateSerpInput(task);
|
|
25543
|
+
return {
|
|
25544
|
+
keyword: validated.keyword,
|
|
25545
|
+
language_code: task.languageCode,
|
|
25546
|
+
...task.locationCode !== void 0 ? { location_code: task.locationCode } : { location_name: validated.locationName },
|
|
25547
|
+
device: task.device,
|
|
25548
|
+
depth: task.depth,
|
|
25549
|
+
tag: task.tag,
|
|
25550
|
+
priority: 1,
|
|
25551
|
+
remove_from_url: ["srsltid"]
|
|
25552
|
+
};
|
|
25553
|
+
});
|
|
25554
|
+
const credentials = await this.getCredentials("serp-task-post");
|
|
25555
|
+
const credentialScope = providerCredentialScope("dataforseo", credentials.login);
|
|
25556
|
+
const price = (await this.pricingForCredentials(credentials, credentialScope)).serpTaskPostPrice;
|
|
25557
|
+
return postDataForSeoSerpTasks({
|
|
25558
|
+
requests,
|
|
25559
|
+
context: input.context,
|
|
25560
|
+
price
|
|
25561
|
+
}, {
|
|
25562
|
+
authorization: this.authorization(credentials),
|
|
25563
|
+
baseUrl: this.baseUrl,
|
|
25564
|
+
fetch: this.fetch,
|
|
25565
|
+
maxResponseBytes: this.maxResponseBytes,
|
|
25566
|
+
timeoutMs: this.timeoutMs,
|
|
25567
|
+
now: this.now,
|
|
25568
|
+
database: this.database,
|
|
25569
|
+
spendLimits: this.spendLimits
|
|
25170
25570
|
});
|
|
25171
25571
|
}
|
|
25572
|
+
async serpTasksReady() {
|
|
25573
|
+
const credentials = await this.getCredentials("serp-tasks-ready");
|
|
25574
|
+
return listReadyDataForSeoSerpTasks(this.serpTaskTransport(credentials));
|
|
25575
|
+
}
|
|
25576
|
+
async serpTaskGet(providerTaskId) {
|
|
25577
|
+
validateDataForSeoSerpTaskId(providerTaskId);
|
|
25578
|
+
const credentials = await this.getCredentials("serp-task-get");
|
|
25579
|
+
return getDataForSeoSerpTask(providerTaskId, this.serpTaskTransport(credentials));
|
|
25580
|
+
}
|
|
25172
25581
|
};
|
|
25173
25582
|
//#endregion
|
|
25174
25583
|
//#region packages/core/src/providers/dataforseo/keyword-mapping.ts
|
|
@@ -26455,7 +26864,7 @@ function pageItem(url, rows) {
|
|
|
26455
26864
|
recommendation: nextRecommendation
|
|
26456
26865
|
};
|
|
26457
26866
|
}
|
|
26458
|
-
function compareItems(left, right) {
|
|
26867
|
+
function compareItems$1(left, right) {
|
|
26459
26868
|
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$4(left.url, right.url);
|
|
26460
26869
|
}
|
|
26461
26870
|
function analyzeSecondPageRows(input) {
|
|
@@ -26500,7 +26909,7 @@ function analyzeSecondPageRows(input) {
|
|
|
26500
26909
|
if (item.impressions >= minImpressions) return true;
|
|
26501
26910
|
selection.belowMinimumPages++;
|
|
26502
26911
|
return false;
|
|
26503
|
-
}).sort(compareItems);
|
|
26912
|
+
}).sort(compareItems$1);
|
|
26504
26913
|
const items = eligibleItems.slice(0, limit);
|
|
26505
26914
|
selection.eligiblePages = eligibleItems.length;
|
|
26506
26915
|
selection.returnedPages = items.length;
|
|
@@ -27326,7 +27735,7 @@ async function defaultCandidates$2() {
|
|
|
27326
27735
|
priority: 10
|
|
27327
27736
|
}];
|
|
27328
27737
|
}
|
|
27329
|
-
function providerError$
|
|
27738
|
+
function providerError$3(error) {
|
|
27330
27739
|
if (!(error instanceof ProviderError)) throw error;
|
|
27331
27740
|
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
27332
27741
|
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
@@ -27394,7 +27803,7 @@ async function keywordResearchReport(input, dependencies = {}) {
|
|
|
27394
27803
|
try {
|
|
27395
27804
|
candidates = dependencies.candidates ?? await defaultCandidates$2();
|
|
27396
27805
|
} catch (error) {
|
|
27397
|
-
return providerError$
|
|
27806
|
+
return providerError$3(error);
|
|
27398
27807
|
}
|
|
27399
27808
|
const resolution = resolveProvider({
|
|
27400
27809
|
capability: "keyword-discovery",
|
|
@@ -27423,7 +27832,7 @@ async function keywordResearchReport(input, dependencies = {}) {
|
|
|
27423
27832
|
}
|
|
27424
27833
|
});
|
|
27425
27834
|
} catch (error) {
|
|
27426
|
-
return providerError$
|
|
27835
|
+
return providerError$3(error);
|
|
27427
27836
|
}
|
|
27428
27837
|
const analysis = evidence.data.map((idea) => ({
|
|
27429
27838
|
keyword: idea.keyword,
|
|
@@ -32060,6 +32469,96 @@ function checkedAt(value, fallback) {
|
|
|
32060
32469
|
const timestamp = Date.parse(value);
|
|
32061
32470
|
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
32062
32471
|
}
|
|
32472
|
+
function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
|
|
32473
|
+
const market = searchMarketSchema.parse(input.market);
|
|
32474
|
+
const keyword = normalizedKeyword$1(input.keyword);
|
|
32475
|
+
const location = locationRequest(market, "serp-snapshot");
|
|
32476
|
+
const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$1(item.keyword) === keyword);
|
|
32477
|
+
if (!result) throw new ProviderError({
|
|
32478
|
+
provider: "dataforseo",
|
|
32479
|
+
operation: "serp-snapshot",
|
|
32480
|
+
code: "invalid-response",
|
|
32481
|
+
message: "DataForSEO returned no matching SERP result."
|
|
32482
|
+
});
|
|
32483
|
+
const warnings = [...snapshot.warnings];
|
|
32484
|
+
const items = result.items ?? [];
|
|
32485
|
+
const organicItems = items.filter((item) => item.type === "organic");
|
|
32486
|
+
const mappedOrganicResults = organicItems.flatMap((item) => {
|
|
32487
|
+
const mapped = organicResult(item);
|
|
32488
|
+
return mapped ? [mapped] : [];
|
|
32489
|
+
}).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
|
|
32490
|
+
const organicResults = mappedOrganicResults.slice(0, input.depth);
|
|
32491
|
+
const invalidRows = organicItems.length - mappedOrganicResults.length;
|
|
32492
|
+
const resultCapped = mappedOrganicResults.length > input.depth;
|
|
32493
|
+
const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
|
|
32494
|
+
if (invalidRows > 0) warnings.push({
|
|
32495
|
+
code: "invalid-organic-results",
|
|
32496
|
+
field: "organicResults",
|
|
32497
|
+
message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
|
|
32498
|
+
});
|
|
32499
|
+
if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
|
|
32500
|
+
code: "invalid-serp-observation-time",
|
|
32501
|
+
field: "checkedAt",
|
|
32502
|
+
message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
|
|
32503
|
+
});
|
|
32504
|
+
const checkUrl = safeUrl(result.check_url);
|
|
32505
|
+
if (result.check_url && !checkUrl) warnings.push({
|
|
32506
|
+
code: "invalid-serp-check-url",
|
|
32507
|
+
field: "checkUrl",
|
|
32508
|
+
message: "DataForSEO returned an invalid SERP check URL."
|
|
32509
|
+
});
|
|
32510
|
+
if (invalidResultCount) warnings.push({
|
|
32511
|
+
code: "invalid-serp-result-count",
|
|
32512
|
+
field: "resultCount",
|
|
32513
|
+
message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
|
|
32514
|
+
});
|
|
32515
|
+
const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
|
|
32516
|
+
return {
|
|
32517
|
+
schemaVersion: 1,
|
|
32518
|
+
provider: "dataforseo",
|
|
32519
|
+
capability: "serp-snapshot",
|
|
32520
|
+
data: {
|
|
32521
|
+
keyword,
|
|
32522
|
+
effectiveKeyword: normalizedKeyword$1(result.spell?.keyword ?? result.keyword),
|
|
32523
|
+
searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
|
|
32524
|
+
checkedAt: checkedAt(result.datetime, snapshot.observedAt),
|
|
32525
|
+
checkUrl,
|
|
32526
|
+
resultCount: invalidResultCount ? null : result.se_results_count ?? null,
|
|
32527
|
+
pagesCount: result.pages_count === void 0 ? null : result.pages_count,
|
|
32528
|
+
features,
|
|
32529
|
+
organicResults
|
|
32530
|
+
},
|
|
32531
|
+
observedAt: snapshot.observedAt,
|
|
32532
|
+
market: {
|
|
32533
|
+
...market,
|
|
32534
|
+
device: market.device ?? "desktop"
|
|
32535
|
+
},
|
|
32536
|
+
coverage: {
|
|
32537
|
+
requestedRows: input.depth,
|
|
32538
|
+
returnedRows: snapshot.returnedRows,
|
|
32539
|
+
retainedRows: organicResults.length,
|
|
32540
|
+
invalidRows,
|
|
32541
|
+
providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
|
|
32542
|
+
completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
|
|
32543
|
+
nextCursor: null
|
|
32544
|
+
},
|
|
32545
|
+
cache: snapshot.cache,
|
|
32546
|
+
cost: snapshot.cost,
|
|
32547
|
+
request: {
|
|
32548
|
+
operation: "serp-snapshot",
|
|
32549
|
+
endpoint,
|
|
32550
|
+
limit: input.depth,
|
|
32551
|
+
filters: {
|
|
32552
|
+
countryCode: market.countryCode,
|
|
32553
|
+
languageCode: market.languageCode,
|
|
32554
|
+
device: market.device ?? "desktop",
|
|
32555
|
+
...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
|
|
32556
|
+
},
|
|
32557
|
+
sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
|
|
32558
|
+
},
|
|
32559
|
+
warnings
|
|
32560
|
+
};
|
|
32561
|
+
}
|
|
32063
32562
|
var DataForSeoSerpSnapshotProvider = class {
|
|
32064
32563
|
provider = "dataforseo";
|
|
32065
32564
|
capabilitySupport = [{
|
|
@@ -32101,7 +32600,7 @@ var DataForSeoSerpSnapshotProvider = class {
|
|
|
32101
32600
|
reportRunId: randomUUID()
|
|
32102
32601
|
};
|
|
32103
32602
|
const location = locationRequest(market, "serp-snapshot");
|
|
32104
|
-
|
|
32603
|
+
return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
|
|
32105
32604
|
keyword,
|
|
32106
32605
|
languageCode: market.languageCode.split("-")[0],
|
|
32107
32606
|
...location,
|
|
@@ -32109,92 +32608,7 @@ var DataForSeoSerpSnapshotProvider = class {
|
|
|
32109
32608
|
depth: input.depth,
|
|
32110
32609
|
refresh: input.refresh,
|
|
32111
32610
|
context
|
|
32112
|
-
});
|
|
32113
|
-
const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$1(item.keyword) === keyword);
|
|
32114
|
-
if (!result) throw new ProviderError({
|
|
32115
|
-
provider: "dataforseo",
|
|
32116
|
-
operation: "serp-snapshot",
|
|
32117
|
-
code: "invalid-response",
|
|
32118
|
-
message: "DataForSEO returned no matching SERP result."
|
|
32119
|
-
});
|
|
32120
|
-
const warnings = [...snapshot.warnings];
|
|
32121
|
-
const items = result.items ?? [];
|
|
32122
|
-
const organicItems = items.filter((item) => item.type === "organic");
|
|
32123
|
-
const mappedOrganicResults = organicItems.flatMap((item) => {
|
|
32124
|
-
const mapped = organicResult(item);
|
|
32125
|
-
return mapped ? [mapped] : [];
|
|
32126
|
-
}).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
|
|
32127
|
-
const organicResults = mappedOrganicResults.slice(0, input.depth);
|
|
32128
|
-
const invalidRows = organicItems.length - mappedOrganicResults.length;
|
|
32129
|
-
const resultCapped = mappedOrganicResults.length > input.depth;
|
|
32130
|
-
const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
|
|
32131
|
-
if (invalidRows > 0) warnings.push({
|
|
32132
|
-
code: "invalid-organic-results",
|
|
32133
|
-
field: "organicResults",
|
|
32134
|
-
message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
|
|
32135
|
-
});
|
|
32136
|
-
if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
|
|
32137
|
-
code: "invalid-serp-observation-time",
|
|
32138
|
-
field: "checkedAt",
|
|
32139
|
-
message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
|
|
32140
|
-
});
|
|
32141
|
-
const checkUrl = safeUrl(result.check_url);
|
|
32142
|
-
if (result.check_url && !checkUrl) warnings.push({
|
|
32143
|
-
code: "invalid-serp-check-url",
|
|
32144
|
-
field: "checkUrl",
|
|
32145
|
-
message: "DataForSEO returned an invalid SERP check URL."
|
|
32146
|
-
});
|
|
32147
|
-
if (invalidResultCount) warnings.push({
|
|
32148
|
-
code: "invalid-serp-result-count",
|
|
32149
|
-
field: "resultCount",
|
|
32150
|
-
message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
|
|
32151
|
-
});
|
|
32152
|
-
const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
|
|
32153
|
-
return {
|
|
32154
|
-
schemaVersion: 1,
|
|
32155
|
-
provider: "dataforseo",
|
|
32156
|
-
capability: "serp-snapshot",
|
|
32157
|
-
data: {
|
|
32158
|
-
keyword,
|
|
32159
|
-
effectiveKeyword: normalizedKeyword$1(result.spell?.keyword ?? result.keyword),
|
|
32160
|
-
searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
|
|
32161
|
-
checkedAt: checkedAt(result.datetime, snapshot.observedAt),
|
|
32162
|
-
checkUrl,
|
|
32163
|
-
resultCount: invalidResultCount ? null : result.se_results_count ?? null,
|
|
32164
|
-
pagesCount: result.pages_count === void 0 ? null : result.pages_count,
|
|
32165
|
-
features,
|
|
32166
|
-
organicResults
|
|
32167
|
-
},
|
|
32168
|
-
observedAt: snapshot.observedAt,
|
|
32169
|
-
market: {
|
|
32170
|
-
...market,
|
|
32171
|
-
device: market.device ?? "desktop"
|
|
32172
|
-
},
|
|
32173
|
-
coverage: {
|
|
32174
|
-
requestedRows: input.depth,
|
|
32175
|
-
returnedRows: snapshot.returnedRows,
|
|
32176
|
-
retainedRows: organicResults.length,
|
|
32177
|
-
invalidRows,
|
|
32178
|
-
providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
|
|
32179
|
-
completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
|
|
32180
|
-
nextCursor: null
|
|
32181
|
-
},
|
|
32182
|
-
cache: snapshot.cache,
|
|
32183
|
-
cost: snapshot.cost,
|
|
32184
|
-
request: {
|
|
32185
|
-
operation: "serp-snapshot",
|
|
32186
|
-
endpoint: SERP_ENDPOINT,
|
|
32187
|
-
limit: input.depth,
|
|
32188
|
-
filters: {
|
|
32189
|
-
countryCode: market.countryCode,
|
|
32190
|
-
languageCode: market.languageCode,
|
|
32191
|
-
device: market.device ?? "desktop",
|
|
32192
|
-
...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
|
|
32193
|
-
},
|
|
32194
|
-
sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
|
|
32195
|
-
},
|
|
32196
|
-
warnings
|
|
32197
|
-
};
|
|
32611
|
+
}));
|
|
32198
32612
|
}
|
|
32199
32613
|
};
|
|
32200
32614
|
//#endregion
|
|
@@ -32213,7 +32627,7 @@ async function defaultCandidates$1() {
|
|
|
32213
32627
|
priority: 10
|
|
32214
32628
|
}];
|
|
32215
32629
|
}
|
|
32216
|
-
function providerError$
|
|
32630
|
+
function providerError$2(error) {
|
|
32217
32631
|
if (!(error instanceof ProviderError)) throw error;
|
|
32218
32632
|
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
32219
32633
|
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
@@ -32275,7 +32689,7 @@ async function serpResultsReport(input, dependencies = {}) {
|
|
|
32275
32689
|
try {
|
|
32276
32690
|
candidates = dependencies.candidates ?? await defaultCandidates$1();
|
|
32277
32691
|
} catch (error) {
|
|
32278
|
-
return providerError$
|
|
32692
|
+
return providerError$2(error);
|
|
32279
32693
|
}
|
|
32280
32694
|
const resolution = resolveProvider({
|
|
32281
32695
|
capability: "serp-snapshot",
|
|
@@ -32300,7 +32714,7 @@ async function serpResultsReport(input, dependencies = {}) {
|
|
|
32300
32714
|
}
|
|
32301
32715
|
});
|
|
32302
32716
|
} catch (error) {
|
|
32303
|
-
return providerError$
|
|
32717
|
+
return providerError$2(error);
|
|
32304
32718
|
}
|
|
32305
32719
|
const domains = domainSummaries(evidence.data);
|
|
32306
32720
|
const dataStatus = reportStatus$1(evidence);
|
|
@@ -32550,6 +32964,1228 @@ async function pseoOpportunitiesReport(input, dependencies = {}) {
|
|
|
32550
32964
|
};
|
|
32551
32965
|
}
|
|
32552
32966
|
//#endregion
|
|
32967
|
+
//#region packages/core/src/keyword-sets/limits.ts
|
|
32968
|
+
const KEYWORD_SET_LIMITS = {
|
|
32969
|
+
setsPerProject: 25,
|
|
32970
|
+
totalSets: 100,
|
|
32971
|
+
keywordsPerSet: 1e4,
|
|
32972
|
+
totalKeywords: 1e5,
|
|
32973
|
+
mutationKeywords: 1e3,
|
|
32974
|
+
tagsPerKeyword: 20,
|
|
32975
|
+
outputRows: 1e3,
|
|
32976
|
+
logicalBytes: 64 * 1024 * 1024
|
|
32977
|
+
};
|
|
32978
|
+
const KEYWORD_SET_FIELD_LIMITS = {
|
|
32979
|
+
name: 80,
|
|
32980
|
+
projectId: 80,
|
|
32981
|
+
sourceReport: 100,
|
|
32982
|
+
keyword: 80,
|
|
32983
|
+
keywordWords: 10,
|
|
32984
|
+
tag: 40,
|
|
32985
|
+
url: 2048,
|
|
32986
|
+
metricJsonBytes: 16 * 1024
|
|
32987
|
+
};
|
|
32988
|
+
//#endregion
|
|
32989
|
+
//#region packages/core/src/keyword-sets/validation.ts
|
|
32990
|
+
const unavailableValueSchema = z.strictObject({
|
|
32991
|
+
state: z.enum([
|
|
32992
|
+
"missing",
|
|
32993
|
+
"unavailable",
|
|
32994
|
+
"invalid"
|
|
32995
|
+
]),
|
|
32996
|
+
value: z.null(),
|
|
32997
|
+
reason: z.string().min(1).max(500)
|
|
32998
|
+
});
|
|
32999
|
+
const providerValueNumberSchema = z.discriminatedUnion("state", [z.strictObject({
|
|
33000
|
+
state: z.literal("observed"),
|
|
33001
|
+
value: z.number().finite()
|
|
33002
|
+
}), unavailableValueSchema]);
|
|
33003
|
+
const providerValueStringSchema = z.discriminatedUnion("state", [z.strictObject({
|
|
33004
|
+
state: z.literal("observed"),
|
|
33005
|
+
value: z.string().max(500)
|
|
33006
|
+
}), unavailableValueSchema]);
|
|
33007
|
+
const monthlySearchSchema = z.strictObject({
|
|
33008
|
+
year: z.number().int().min(2e3).max(2200),
|
|
33009
|
+
month: z.number().int().min(1).max(12),
|
|
33010
|
+
searchVolume: z.number().finite().nonnegative()
|
|
33011
|
+
});
|
|
33012
|
+
const providerValueMonthlySearchesSchema = z.discriminatedUnion("state", [z.strictObject({
|
|
33013
|
+
state: z.literal("observed"),
|
|
33014
|
+
value: z.array(monthlySearchSchema).max(24)
|
|
33015
|
+
}), unavailableValueSchema]);
|
|
33016
|
+
const keywordMetricSchema = z.strictObject({
|
|
33017
|
+
keyword: z.string().min(1).max(KEYWORD_SET_FIELD_LIMITS.keyword),
|
|
33018
|
+
monthlySearchVolume: providerValueNumberSchema,
|
|
33019
|
+
monthlySearches: providerValueMonthlySearchesSchema,
|
|
33020
|
+
searchVolumeUpdatedAt: providerValueStringSchema,
|
|
33021
|
+
cpcUsd: providerValueNumberSchema,
|
|
33022
|
+
paidCompetition: providerValueNumberSchema,
|
|
33023
|
+
keywordDifficulty: providerValueNumberSchema,
|
|
33024
|
+
intent: providerValueStringSchema,
|
|
33025
|
+
resultCount: providerValueNumberSchema
|
|
33026
|
+
});
|
|
33027
|
+
const savedMetricSchema = z.strictObject({
|
|
33028
|
+
schemaVersion: z.literal(1),
|
|
33029
|
+
provider: providerIdSchema,
|
|
33030
|
+
observedAt: z.iso.datetime(),
|
|
33031
|
+
metric: keywordMetricSchema
|
|
33032
|
+
});
|
|
33033
|
+
function invalid$1(message) {
|
|
33034
|
+
throw new SeoError("INVALID_INPUT", message);
|
|
33035
|
+
}
|
|
33036
|
+
function boundedText(value, label, max) {
|
|
33037
|
+
const normalized = value.trim();
|
|
33038
|
+
if (!normalized || normalized.length > max) invalid$1(`${label} must contain 1 to ${max} characters.`);
|
|
33039
|
+
return normalized;
|
|
33040
|
+
}
|
|
33041
|
+
function normalizeSavedKeyword(value) {
|
|
33042
|
+
const normalized = value.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
33043
|
+
if (!normalized || normalized.length > KEYWORD_SET_FIELD_LIMITS.keyword || normalized.split(" ").length > KEYWORD_SET_FIELD_LIMITS.keywordWords) invalid$1(`Keywords must contain 1 to ${KEYWORD_SET_FIELD_LIMITS.keyword} characters and at most ${KEYWORD_SET_FIELD_LIMITS.keywordWords} words.`);
|
|
33044
|
+
return normalized;
|
|
33045
|
+
}
|
|
33046
|
+
function displayKeyword(value) {
|
|
33047
|
+
normalizeSavedKeyword(value);
|
|
33048
|
+
return value.trim().replace(/\s+/gu, " ");
|
|
33049
|
+
}
|
|
33050
|
+
function normalizedTags(values = []) {
|
|
33051
|
+
if (values.length > KEYWORD_SET_LIMITS.tagsPerKeyword) invalid$1(`A keyword can have at most ${KEYWORD_SET_LIMITS.tagsPerKeyword} tags.`);
|
|
33052
|
+
const tags = [...new Set(values.map((value) => boundedText(value, "Tag", KEYWORD_SET_FIELD_LIMITS.tag).toLowerCase()))].sort();
|
|
33053
|
+
if (tags.length > KEYWORD_SET_LIMITS.tagsPerKeyword) invalid$1(`A keyword can have at most ${KEYWORD_SET_LIMITS.tagsPerKeyword} tags.`);
|
|
33054
|
+
return tags;
|
|
33055
|
+
}
|
|
33056
|
+
function pageMapping(value) {
|
|
33057
|
+
if (value === null || value === void 0) return value;
|
|
33058
|
+
if (value.kind !== "target" && value.kind !== "proposed") invalid$1("Page mapping kind must be target or proposed.");
|
|
33059
|
+
const raw = boundedText(value.url, "Page URL", KEYWORD_SET_FIELD_LIMITS.url);
|
|
33060
|
+
let url;
|
|
33061
|
+
try {
|
|
33062
|
+
url = new URL(raw);
|
|
33063
|
+
} catch {
|
|
33064
|
+
invalid$1("Page mapping must use an absolute HTTP or HTTPS URL.");
|
|
33065
|
+
}
|
|
33066
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") invalid$1("Page mapping must use an absolute HTTP or HTTPS URL.");
|
|
33067
|
+
return {
|
|
33068
|
+
kind: value.kind,
|
|
33069
|
+
url: url.toString()
|
|
33070
|
+
};
|
|
33071
|
+
}
|
|
33072
|
+
function savedMetric(value, normalizedKeyword) {
|
|
33073
|
+
if (value === null || value === void 0) return {
|
|
33074
|
+
value,
|
|
33075
|
+
json: null
|
|
33076
|
+
};
|
|
33077
|
+
const parsed = savedMetricSchema.safeParse(value);
|
|
33078
|
+
if (!parsed.success) invalid$1("Use a valid normalized keyword metric snapshot.");
|
|
33079
|
+
if (normalizeSavedKeyword(parsed.data.metric.keyword) !== normalizedKeyword) invalid$1("A saved metric must describe the same keyword as the set item.");
|
|
33080
|
+
const json = JSON.stringify(parsed.data);
|
|
33081
|
+
if (Buffer.byteLength(json) > KEYWORD_SET_FIELD_LIMITS.metricJsonBytes) invalid$1(`A saved keyword metric cannot exceed ${KEYWORD_SET_FIELD_LIMITS.metricJsonBytes} bytes.`);
|
|
33082
|
+
return {
|
|
33083
|
+
value: parsed.data,
|
|
33084
|
+
json
|
|
33085
|
+
};
|
|
33086
|
+
}
|
|
33087
|
+
function validateMutationItems(items) {
|
|
33088
|
+
if (items.length < 1 || items.length > KEYWORD_SET_LIMITS.mutationKeywords) invalid$1(`Add 1 to ${KEYWORD_SET_LIMITS.mutationKeywords} keywords per operation.`);
|
|
33089
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
33090
|
+
for (const item of items) {
|
|
33091
|
+
const key = normalizeSavedKeyword(item.keyword);
|
|
33092
|
+
const existing = normalized.get(key);
|
|
33093
|
+
const tags = normalizedTags([...existing?.tags ?? [], ...item.tags ?? []]);
|
|
33094
|
+
const page = pageMapping(item.page !== void 0 ? item.page : existing?.page);
|
|
33095
|
+
const metric = savedMetric(item.latestMetric !== void 0 ? item.latestMetric : existing?.metric, key);
|
|
33096
|
+
normalized.set(key, {
|
|
33097
|
+
keyword: existing?.keyword ?? displayKeyword(item.keyword),
|
|
33098
|
+
tags,
|
|
33099
|
+
page,
|
|
33100
|
+
metric: metric.value,
|
|
33101
|
+
metricJson: metric.json
|
|
33102
|
+
});
|
|
33103
|
+
}
|
|
33104
|
+
return normalized;
|
|
33105
|
+
}
|
|
33106
|
+
function parseStoredJson(value, schema, label) {
|
|
33107
|
+
let decoded;
|
|
33108
|
+
try {
|
|
33109
|
+
decoded = JSON.parse(value);
|
|
33110
|
+
} catch {
|
|
33111
|
+
throw new SeoError("INTERNAL_ERROR", `${label} contains invalid JSON.`);
|
|
33112
|
+
}
|
|
33113
|
+
const parsed = schema.safeParse(decoded);
|
|
33114
|
+
if (!parsed.success) throw new SeoError("INTERNAL_ERROR", `${label} has an invalid shape.`);
|
|
33115
|
+
return parsed.data;
|
|
33116
|
+
}
|
|
33117
|
+
//#endregion
|
|
33118
|
+
//#region packages/core/src/keyword-sets/rows.ts
|
|
33119
|
+
const SET_SELECT = `
|
|
33120
|
+
SELECT sets.*,
|
|
33121
|
+
(SELECT COUNT(*) FROM keyword_set_items items WHERE items.set_id = sets.id) AS keyword_count,
|
|
33122
|
+
(SELECT COUNT(DISTINCT tags.tag) FROM keyword_set_tags tags WHERE tags.set_id = sets.id) AS tag_count
|
|
33123
|
+
FROM keyword_sets sets`;
|
|
33124
|
+
function storeDatabase(options) {
|
|
33125
|
+
return options.database ?? getDb();
|
|
33126
|
+
}
|
|
33127
|
+
function rowToSet(row) {
|
|
33128
|
+
const provider = row.provider ? providerIdSchema.safeParse(row.provider) : void 0;
|
|
33129
|
+
if (provider && !provider.success) throw new SeoError("INTERNAL_ERROR", "Saved keyword set provider is invalid.");
|
|
33130
|
+
return {
|
|
33131
|
+
schemaVersion: 1,
|
|
33132
|
+
id: row.id,
|
|
33133
|
+
projectId: row.project_id,
|
|
33134
|
+
name: row.name,
|
|
33135
|
+
market: parseStoredJson(row.market_json, searchMarketSchema, "Saved search market"),
|
|
33136
|
+
provider: provider?.data ?? null,
|
|
33137
|
+
sourceReport: row.source_report,
|
|
33138
|
+
keywordCount: row.keyword_count,
|
|
33139
|
+
tagCount: row.tag_count,
|
|
33140
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
33141
|
+
updatedAt: new Date(row.updated_at).toISOString(),
|
|
33142
|
+
lastRefreshedAt: row.last_refreshed_at === null ? null : new Date(row.last_refreshed_at).toISOString()
|
|
33143
|
+
};
|
|
33144
|
+
}
|
|
33145
|
+
function findSetRow(db, projectId, idOrName) {
|
|
33146
|
+
return db.prepare(`${SET_SELECT}
|
|
33147
|
+
WHERE sets.project_id = ?
|
|
33148
|
+
AND (sets.id = ? OR sets.name = ? COLLATE NOCASE)
|
|
33149
|
+
ORDER BY CASE WHEN sets.id = ? THEN 0 ELSE 1 END
|
|
33150
|
+
LIMIT 1`).get(projectId, idOrName, idOrName, idOrName);
|
|
33151
|
+
}
|
|
33152
|
+
function requireSetRow(db, projectId, idOrName) {
|
|
33153
|
+
const row = findSetRow(db, projectId, idOrName);
|
|
33154
|
+
if (!row) invalid$1(`Keyword set not found: ${idOrName}`);
|
|
33155
|
+
return row;
|
|
33156
|
+
}
|
|
33157
|
+
function rowCount(db, sql, ...params) {
|
|
33158
|
+
return db.prepare(sql).get(...params).count;
|
|
33159
|
+
}
|
|
33160
|
+
function keywordSetLogicalBytes(db = getDb()) {
|
|
33161
|
+
return db.prepare(`SELECT
|
|
33162
|
+
COALESCE((SELECT SUM(length(id) + length(project_id) + length(name) + length(market_json) + COALESCE(length(provider), 0) + COALESCE(length(source_report), 0)) FROM keyword_sets), 0) +
|
|
33163
|
+
COALESCE((SELECT SUM(length(set_id) + length(normalized_keyword) + length(display_keyword) + COALESCE(length(metric_json), 0) + COALESCE(length(metric_provider), 0) + COALESCE(length(metric_observed_at), 0) + COALESCE(length(page_kind), 0) + COALESCE(length(page_url), 0)) FROM keyword_set_items), 0) +
|
|
33164
|
+
COALESCE((SELECT SUM(length(set_id) + length(normalized_keyword) + length(tag)) FROM keyword_set_tags), 0)
|
|
33165
|
+
AS bytes`).get().bytes;
|
|
33166
|
+
}
|
|
33167
|
+
function enforceKeywordSetLogicalBytes(db, maxLogicalBytes = KEYWORD_SET_LIMITS.logicalBytes) {
|
|
33168
|
+
if (keywordSetLogicalBytes(db) > maxLogicalBytes) invalid$1(`Saved keyword data cannot exceed ${maxLogicalBytes} logical bytes. Remove an unused set before adding more.`);
|
|
33169
|
+
}
|
|
33170
|
+
//#endregion
|
|
33171
|
+
//#region packages/core/src/keyword-sets/store.ts
|
|
33172
|
+
function createKeywordSet(input, options = {}) {
|
|
33173
|
+
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33174
|
+
const name = boundedText(input.name, "Keyword set name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33175
|
+
const market = searchMarketSchema.safeParse(input.market);
|
|
33176
|
+
if (!market.success) invalid$1("Use a valid search market.");
|
|
33177
|
+
const provider = input.provider === void 0 ? void 0 : providerIdSchema.safeParse(input.provider);
|
|
33178
|
+
if (provider && !provider.success) invalid$1("Use a supported keyword provider.");
|
|
33179
|
+
const sourceReport = input.sourceReport ? boundedText(input.sourceReport, "Source report", KEYWORD_SET_FIELD_LIMITS.sourceReport) : null;
|
|
33180
|
+
const db = storeDatabase(options);
|
|
33181
|
+
if (rowCount(db, "SELECT COUNT(*) AS count FROM keyword_sets") >= KEYWORD_SET_LIMITS.totalSets) invalid$1(`At most ${KEYWORD_SET_LIMITS.totalSets} keyword sets can be saved.`);
|
|
33182
|
+
if (rowCount(db, "SELECT COUNT(*) AS count FROM keyword_sets WHERE project_id = ?", projectId) >= KEYWORD_SET_LIMITS.setsPerProject) invalid$1(`A project can have at most ${KEYWORD_SET_LIMITS.setsPerProject} keyword sets.`);
|
|
33183
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33184
|
+
const id = (options.id ?? randomUUID)();
|
|
33185
|
+
try {
|
|
33186
|
+
db.transaction(() => {
|
|
33187
|
+
db.prepare(`INSERT INTO keyword_sets
|
|
33188
|
+
(id, project_id, name, market_json, provider, source_report, created_at, updated_at)
|
|
33189
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, projectId, name, JSON.stringify(market.data), provider?.data ?? null, sourceReport, now, now);
|
|
33190
|
+
enforceKeywordSetLogicalBytes(db, options.maxLogicalBytes);
|
|
33191
|
+
}).immediate();
|
|
33192
|
+
} catch (error) {
|
|
33193
|
+
if (/unique constraint/i.test(String(error))) invalid$1(`A keyword set named ${name} already exists for this project.`);
|
|
33194
|
+
throw error;
|
|
33195
|
+
}
|
|
33196
|
+
return rowToSet(requireSetRow(db, projectId, id));
|
|
33197
|
+
}
|
|
33198
|
+
function listKeywordSets(input, options = {}) {
|
|
33199
|
+
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33200
|
+
const limit = input.limit ?? 100;
|
|
33201
|
+
const offset = input.offset ?? 0;
|
|
33202
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) invalid$1("Keyword set list limit must be from 1 to 100.");
|
|
33203
|
+
if (!Number.isSafeInteger(offset) || offset < 0) invalid$1("Keyword set list offset must be zero or greater.");
|
|
33204
|
+
return storeDatabase(options).prepare(`${SET_SELECT}
|
|
33205
|
+
WHERE sets.project_id = ?
|
|
33206
|
+
ORDER BY sets.updated_at DESC, sets.id
|
|
33207
|
+
LIMIT ? OFFSET ?`).all(projectId, limit, offset).map(rowToSet);
|
|
33208
|
+
}
|
|
33209
|
+
function getKeywordSet(input, options = {}) {
|
|
33210
|
+
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33211
|
+
const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33212
|
+
const tag = input.tag ? boundedText(input.tag, "Tag", KEYWORD_SET_FIELD_LIMITS.tag).toLowerCase() : null;
|
|
33213
|
+
const limit = input.limit ?? 100;
|
|
33214
|
+
const offset = input.offset ?? 0;
|
|
33215
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > KEYWORD_SET_LIMITS.outputRows) invalid$1(`Keyword set output limit must be from 1 to ${KEYWORD_SET_LIMITS.outputRows}.`);
|
|
33216
|
+
if (!Number.isSafeInteger(offset) || offset < 0) invalid$1("Keyword set output offset must be zero or greater.");
|
|
33217
|
+
const db = storeDatabase(options);
|
|
33218
|
+
const setRow = requireSetRow(db, projectId, idOrName);
|
|
33219
|
+
const filterSql = tag ? `AND EXISTS (
|
|
33220
|
+
SELECT 1 FROM keyword_set_tags filter_tags
|
|
33221
|
+
WHERE filter_tags.set_id = items.set_id
|
|
33222
|
+
AND filter_tags.normalized_keyword = items.normalized_keyword
|
|
33223
|
+
AND filter_tags.tag = ?
|
|
33224
|
+
)` : "";
|
|
33225
|
+
const params = tag ? [setRow.id, tag] : [setRow.id];
|
|
33226
|
+
const total = rowCount(db, `SELECT COUNT(*) AS count FROM keyword_set_items items
|
|
33227
|
+
WHERE items.set_id = ? ${filterSql}`, ...params);
|
|
33228
|
+
const rows = db.prepare(`SELECT items.* FROM keyword_set_items items
|
|
33229
|
+
WHERE items.set_id = ? ${filterSql}
|
|
33230
|
+
ORDER BY items.normalized_keyword
|
|
33231
|
+
LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
33232
|
+
const tagsByKeyword = loadTags(db, setRow.id, rows);
|
|
33233
|
+
const items = rows.map((row) => itemFromRow(row, tagsByKeyword.get(row.normalized_keyword) ?? []));
|
|
33234
|
+
return {
|
|
33235
|
+
schemaVersion: 1,
|
|
33236
|
+
set: rowToSet(setRow),
|
|
33237
|
+
items,
|
|
33238
|
+
pagination: {
|
|
33239
|
+
offset,
|
|
33240
|
+
limit,
|
|
33241
|
+
returned: items.length,
|
|
33242
|
+
total,
|
|
33243
|
+
nextOffset: offset + items.length < total ? offset + items.length : null
|
|
33244
|
+
},
|
|
33245
|
+
filter: { tag }
|
|
33246
|
+
};
|
|
33247
|
+
}
|
|
33248
|
+
function loadTags(db, setId, rows) {
|
|
33249
|
+
if (rows.length === 0) return /* @__PURE__ */ new Map();
|
|
33250
|
+
const tagRows = db.prepare(`SELECT normalized_keyword, tag FROM keyword_set_tags
|
|
33251
|
+
WHERE set_id = ? AND normalized_keyword IN (${rows.map(() => "?").join(", ")})
|
|
33252
|
+
ORDER BY normalized_keyword, tag`).all(setId, ...rows.map((row) => row.normalized_keyword));
|
|
33253
|
+
const tagsByKeyword = /* @__PURE__ */ new Map();
|
|
33254
|
+
for (const row of tagRows) {
|
|
33255
|
+
const tags = tagsByKeyword.get(row.normalized_keyword) ?? [];
|
|
33256
|
+
tags.push(row.tag);
|
|
33257
|
+
tagsByKeyword.set(row.normalized_keyword, tags);
|
|
33258
|
+
}
|
|
33259
|
+
return tagsByKeyword;
|
|
33260
|
+
}
|
|
33261
|
+
function itemFromRow(row, tags) {
|
|
33262
|
+
const latestMetric = row.metric_json ? parseStoredJson(row.metric_json, savedMetricSchema, "Saved keyword metric") : null;
|
|
33263
|
+
if (latestMetric && (latestMetric.provider !== row.metric_provider || latestMetric.observedAt !== row.metric_observed_at || normalizeSavedKeyword(latestMetric.metric.keyword) !== row.normalized_keyword)) throw new SeoError("INTERNAL_ERROR", "Saved keyword metric metadata is inconsistent.");
|
|
33264
|
+
const page = row.page_kind && row.page_url ? {
|
|
33265
|
+
kind: row.page_kind,
|
|
33266
|
+
url: row.page_url
|
|
33267
|
+
} : null;
|
|
33268
|
+
return {
|
|
33269
|
+
keyword: row.display_keyword,
|
|
33270
|
+
normalizedKeyword: row.normalized_keyword,
|
|
33271
|
+
tags,
|
|
33272
|
+
page,
|
|
33273
|
+
latestMetric,
|
|
33274
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
33275
|
+
updatedAt: new Date(row.updated_at).toISOString()
|
|
33276
|
+
};
|
|
33277
|
+
}
|
|
33278
|
+
function deleteKeywordSet(input, options = {}) {
|
|
33279
|
+
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33280
|
+
const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33281
|
+
const db = storeDatabase(options);
|
|
33282
|
+
const row = findSetRow(db, projectId, idOrName);
|
|
33283
|
+
if (!row) return false;
|
|
33284
|
+
return db.prepare("DELETE FROM keyword_sets WHERE id = ?").run(row.id).changes > 0;
|
|
33285
|
+
}
|
|
33286
|
+
//#endregion
|
|
33287
|
+
//#region packages/core/src/rank-tracking/dataforseo-collector.ts
|
|
33288
|
+
const QUEUED_RESULT_ENDPOINT = "v3/serp/google/organic/task_get/advanced/{taskId}";
|
|
33289
|
+
var DataForSeoRankTrackingCollector = class {
|
|
33290
|
+
provider = "dataforseo";
|
|
33291
|
+
client;
|
|
33292
|
+
liveProvider;
|
|
33293
|
+
constructor(options = {}) {
|
|
33294
|
+
this.client = new DataForSeoClient(options);
|
|
33295
|
+
this.liveProvider = new DataForSeoSerpSnapshotProvider({ client: this.client });
|
|
33296
|
+
}
|
|
33297
|
+
live(input) {
|
|
33298
|
+
return this.liveProvider.serpSnapshot(input);
|
|
33299
|
+
}
|
|
33300
|
+
async post(input) {
|
|
33301
|
+
const snapshot = await this.client.serpTaskPost({
|
|
33302
|
+
tasks: input.tasks.map(({ taskKey, request }) => {
|
|
33303
|
+
const market = request.market;
|
|
33304
|
+
return {
|
|
33305
|
+
tag: taskKey,
|
|
33306
|
+
keyword: normalizedKeyword$1(request.keyword),
|
|
33307
|
+
languageCode: market.languageCode.split("-")[0],
|
|
33308
|
+
...locationRequest(market, "serp-snapshot"),
|
|
33309
|
+
device: market.device ?? "desktop",
|
|
33310
|
+
depth: request.depth
|
|
33311
|
+
};
|
|
33312
|
+
}),
|
|
33313
|
+
context: input.context
|
|
33314
|
+
});
|
|
33315
|
+
return {
|
|
33316
|
+
provider: this.provider,
|
|
33317
|
+
receipts: snapshot.taskReceipts.map((receipt) => ({
|
|
33318
|
+
taskKey: receipt.tag,
|
|
33319
|
+
providerTaskId: receipt.providerTaskId
|
|
33320
|
+
})),
|
|
33321
|
+
estimatedCostMicros: snapshot.estimatedCostMicros,
|
|
33322
|
+
actualCostMicros: snapshot.actualCostMicros,
|
|
33323
|
+
warnings: snapshot.warnings
|
|
33324
|
+
};
|
|
33325
|
+
}
|
|
33326
|
+
ready() {
|
|
33327
|
+
return this.client.serpTasksReady().then((tasks) => tasks.map((task) => ({
|
|
33328
|
+
providerTaskId: task.providerTaskId,
|
|
33329
|
+
taskKey: task.tag
|
|
33330
|
+
})));
|
|
33331
|
+
}
|
|
33332
|
+
async collect(input) {
|
|
33333
|
+
const snapshot = await this.client.serpTaskGet(input.providerTaskId);
|
|
33334
|
+
return mapDataForSeoSerpSnapshot(input.request, snapshot, QUEUED_RESULT_ENDPOINT);
|
|
33335
|
+
}
|
|
33336
|
+
};
|
|
33337
|
+
//#endregion
|
|
33338
|
+
//#region packages/core/src/rank-tracking/limits.ts
|
|
33339
|
+
const RANK_TRACKING_LIMITS = {
|
|
33340
|
+
configurationsPerProject: 25,
|
|
33341
|
+
totalConfigurations: 100,
|
|
33342
|
+
liveKeywordsPerRun: 50,
|
|
33343
|
+
queuedKeywordsPerRun: 1e3,
|
|
33344
|
+
devicesPerRun: 2,
|
|
33345
|
+
tasksPerProviderPost: 100,
|
|
33346
|
+
retainedRunsPerConfiguration: 90,
|
|
33347
|
+
outputRows: 250,
|
|
33348
|
+
logicalBytes: 256 * 1024 * 1024
|
|
33349
|
+
};
|
|
33350
|
+
//#endregion
|
|
33351
|
+
//#region packages/core/src/rank-tracking/configuration-store.ts
|
|
33352
|
+
function invalid(message) {
|
|
33353
|
+
throw new SeoError("INVALID_INPUT", message);
|
|
33354
|
+
}
|
|
33355
|
+
function database(options) {
|
|
33356
|
+
return options.database ?? getDb();
|
|
33357
|
+
}
|
|
33358
|
+
function parseJson(value, label) {
|
|
33359
|
+
try {
|
|
33360
|
+
return JSON.parse(value);
|
|
33361
|
+
} catch {
|
|
33362
|
+
throw new SeoError("INTERNAL_ERROR", `${label} is corrupt.`);
|
|
33363
|
+
}
|
|
33364
|
+
}
|
|
33365
|
+
function normalizeDomain(value) {
|
|
33366
|
+
const raw = value.trim().toLowerCase();
|
|
33367
|
+
if (!raw || raw.length > 253) invalid("Use a valid target domain.");
|
|
33368
|
+
let hostname;
|
|
33369
|
+
try {
|
|
33370
|
+
hostname = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname;
|
|
33371
|
+
} catch {
|
|
33372
|
+
invalid("Use a valid target domain.");
|
|
33373
|
+
}
|
|
33374
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..") || !/^[a-z0-9.-]+$/u.test(hostname)) invalid("Use a valid target domain.");
|
|
33375
|
+
return hostname.replace(/\.$/u, "");
|
|
33376
|
+
}
|
|
33377
|
+
function targetMatchesDomain(targetDomain, candidate) {
|
|
33378
|
+
const target = normalizeDomain(targetDomain);
|
|
33379
|
+
const host = normalizeDomain(candidate);
|
|
33380
|
+
return host === target || host.endsWith(`.${target}`);
|
|
33381
|
+
}
|
|
33382
|
+
function configFromRow(row) {
|
|
33383
|
+
const market = searchMarketSchema.safeParse(parseJson(row.market_json, "Saved rank market"));
|
|
33384
|
+
const devices = parseJson(row.devices_json, "Saved rank devices");
|
|
33385
|
+
const provider = providerIdSchema.safeParse(row.provider);
|
|
33386
|
+
if (!market.success || market.data.device !== void 0 || !Array.isArray(devices) || devices.length < 1 || devices.length > 2 || devices.some((item) => item !== "desktop" && item !== "mobile") || !provider.success) throw new SeoError("INTERNAL_ERROR", "Saved rank tracking configuration is invalid.");
|
|
33387
|
+
return {
|
|
33388
|
+
schemaVersion: 1,
|
|
33389
|
+
id: row.id,
|
|
33390
|
+
projectId: row.project_id,
|
|
33391
|
+
keywordSetId: row.keyword_set_id,
|
|
33392
|
+
targetDomain: row.target_domain,
|
|
33393
|
+
tag: row.tag,
|
|
33394
|
+
market: market.data,
|
|
33395
|
+
devices,
|
|
33396
|
+
provider: provider.data,
|
|
33397
|
+
collectionMethod: row.collection_method,
|
|
33398
|
+
cadence: row.cadence,
|
|
33399
|
+
depth: row.depth,
|
|
33400
|
+
keywordLimit: row.keyword_limit,
|
|
33401
|
+
nextRunAt: row.next_run_at === null ? null : new Date(row.next_run_at).toISOString(),
|
|
33402
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
33403
|
+
updatedAt: new Date(row.updated_at).toISOString()
|
|
33404
|
+
};
|
|
33405
|
+
}
|
|
33406
|
+
function runFromRow(row) {
|
|
33407
|
+
return {
|
|
33408
|
+
schemaVersion: 1,
|
|
33409
|
+
id: row.id,
|
|
33410
|
+
configId: row.config_id,
|
|
33411
|
+
state: row.state,
|
|
33412
|
+
collectionMethod: row.collection_method,
|
|
33413
|
+
scheduledFor: new Date(row.scheduled_for).toISOString(),
|
|
33414
|
+
startedAt: new Date(row.started_at).toISOString(),
|
|
33415
|
+
completedAt: row.completed_at === null ? null : new Date(row.completed_at).toISOString(),
|
|
33416
|
+
keywordCount: row.keyword_count,
|
|
33417
|
+
taskCount: row.task_count,
|
|
33418
|
+
snapshotCount: row.snapshot_count,
|
|
33419
|
+
pendingCount: row.pending_count,
|
|
33420
|
+
failedCount: row.failed_count,
|
|
33421
|
+
estimatedCostMicros: row.estimated_cost_micros,
|
|
33422
|
+
actualCostMicros: row.actual_cost_micros,
|
|
33423
|
+
errorSummary: row.error_summary
|
|
33424
|
+
};
|
|
33425
|
+
}
|
|
33426
|
+
function taskFromRow(row) {
|
|
33427
|
+
return {
|
|
33428
|
+
id: row.id,
|
|
33429
|
+
runId: row.run_id,
|
|
33430
|
+
normalizedKeyword: row.normalized_keyword,
|
|
33431
|
+
displayKeyword: row.display_keyword,
|
|
33432
|
+
device: row.device,
|
|
33433
|
+
state: row.state,
|
|
33434
|
+
providerTaskId: row.provider_task_id,
|
|
33435
|
+
attemptCount: row.attempt_count,
|
|
33436
|
+
errorCode: row.error_code,
|
|
33437
|
+
errorMessage: row.error_message
|
|
33438
|
+
};
|
|
33439
|
+
}
|
|
33440
|
+
function observationFromRow(row) {
|
|
33441
|
+
const provider = providerIdSchema.safeParse(row.provider);
|
|
33442
|
+
if (!provider.success) throw new SeoError("INTERNAL_ERROR", "Saved rank provider is invalid.");
|
|
33443
|
+
return {
|
|
33444
|
+
taskId: row.task_id,
|
|
33445
|
+
runId: row.run_id,
|
|
33446
|
+
keyword: row.display_keyword,
|
|
33447
|
+
normalizedKeyword: row.normalized_keyword,
|
|
33448
|
+
device: row.device,
|
|
33449
|
+
state: row.observation_state,
|
|
33450
|
+
organicPosition: row.organic_position,
|
|
33451
|
+
absolutePosition: row.absolute_position,
|
|
33452
|
+
rankingUrl: row.ranking_url,
|
|
33453
|
+
observedFeatures: parseJson(row.observed_features_json, "Saved rank features"),
|
|
33454
|
+
checkedAt: row.checked_at,
|
|
33455
|
+
provider: provider.data,
|
|
33456
|
+
providerTaskId: row.provider_task_id,
|
|
33457
|
+
requestedDepth: row.requested_depth,
|
|
33458
|
+
returnedRows: row.returned_rows,
|
|
33459
|
+
retainedRows: row.retained_rows,
|
|
33460
|
+
invalidRows: row.invalid_rows,
|
|
33461
|
+
completeness: row.completeness,
|
|
33462
|
+
estimatedCostMicros: row.estimated_cost_micros,
|
|
33463
|
+
actualCostMicros: row.actual_cost_micros,
|
|
33464
|
+
warnings: parseJson(row.warnings_json, "Saved rank warnings")
|
|
33465
|
+
};
|
|
33466
|
+
}
|
|
33467
|
+
function stableConfigKey(value) {
|
|
33468
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
33469
|
+
}
|
|
33470
|
+
function nextRunAt(cadence, scheduledFor) {
|
|
33471
|
+
if (cadence === "manual") return null;
|
|
33472
|
+
const next = new Date(scheduledFor);
|
|
33473
|
+
if (cadence === "daily") next.setUTCDate(next.getUTCDate() + 1);
|
|
33474
|
+
if (cadence === "weekly") next.setUTCDate(next.getUTCDate() + 7);
|
|
33475
|
+
if (cadence === "monthly") {
|
|
33476
|
+
const desiredDay = next.getUTCDate();
|
|
33477
|
+
next.setUTCDate(1);
|
|
33478
|
+
next.setUTCMonth(next.getUTCMonth() + 1);
|
|
33479
|
+
const lastDay = new Date(Date.UTC(next.getUTCFullYear(), next.getUTCMonth() + 1, 0)).getUTCDate();
|
|
33480
|
+
next.setUTCDate(Math.min(desiredDay, lastDay));
|
|
33481
|
+
}
|
|
33482
|
+
return next.getTime();
|
|
33483
|
+
}
|
|
33484
|
+
function getOrCreateRankTrackingConfiguration(input, options = {}) {
|
|
33485
|
+
const projectId = input.projectId.trim();
|
|
33486
|
+
const keywordSetId = input.keywordSetId.trim();
|
|
33487
|
+
const tag = input.tag?.trim().toLowerCase() || null;
|
|
33488
|
+
if (!projectId || projectId.length > 80) invalid("Use a valid project id.");
|
|
33489
|
+
if (!keywordSetId || keywordSetId.length > 80) invalid("Use a valid keyword set id.");
|
|
33490
|
+
if (tag && tag.length > 40) invalid("Rank tracking tag is too long.");
|
|
33491
|
+
const market = searchMarketSchema.safeParse(input.market);
|
|
33492
|
+
if (!market.success) invalid("Use a valid rank tracking market.");
|
|
33493
|
+
const { device: _ignoredDevice, ...marketWithoutDevice } = market.data;
|
|
33494
|
+
const devices = [...new Set(input.devices)].sort();
|
|
33495
|
+
if (devices.length < 1 || devices.length > 2 || devices.some((item) => item !== "desktop" && item !== "mobile")) invalid("Track desktop, mobile, or both devices.");
|
|
33496
|
+
const provider = providerIdSchema.safeParse(input.provider);
|
|
33497
|
+
if (!provider.success) invalid("Use a supported rank tracking provider.");
|
|
33498
|
+
if (!["live", "queued"].includes(input.collectionMethod)) invalid("Use live or queued rank collection.");
|
|
33499
|
+
if (![
|
|
33500
|
+
"manual",
|
|
33501
|
+
"daily",
|
|
33502
|
+
"weekly",
|
|
33503
|
+
"monthly"
|
|
33504
|
+
].includes(input.cadence)) invalid("Use a supported rank tracking cadence.");
|
|
33505
|
+
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > 100) invalid("Rank tracking depth must be from 1 to 100.");
|
|
33506
|
+
const maxKeywords = input.collectionMethod === "live" ? RANK_TRACKING_LIMITS.liveKeywordsPerRun : RANK_TRACKING_LIMITS.queuedKeywordsPerRun;
|
|
33507
|
+
if (!Number.isSafeInteger(input.keywordLimit) || input.keywordLimit < 1 || input.keywordLimit > maxKeywords) invalid(`${input.collectionMethod === "live" ? "Live" : "Queued"} rank tracking can collect 1 to ${maxKeywords} keywords per run.`);
|
|
33508
|
+
const targetDomain = normalizeDomain(input.targetDomain);
|
|
33509
|
+
const db = database(options);
|
|
33510
|
+
const set = getKeywordSet({
|
|
33511
|
+
projectId,
|
|
33512
|
+
idOrName: keywordSetId,
|
|
33513
|
+
limit: 1
|
|
33514
|
+
}, { database: db }).set;
|
|
33515
|
+
if (set.market.searchEngine !== marketWithoutDevice.searchEngine) invalid("The rank tracking search engine must match the keyword set.");
|
|
33516
|
+
const configKey = stableConfigKey({
|
|
33517
|
+
projectId,
|
|
33518
|
+
keywordSetId: set.id,
|
|
33519
|
+
targetDomain,
|
|
33520
|
+
tag,
|
|
33521
|
+
market: marketWithoutDevice,
|
|
33522
|
+
devices,
|
|
33523
|
+
provider: provider.data,
|
|
33524
|
+
collectionMethod: input.collectionMethod,
|
|
33525
|
+
cadence: input.cadence,
|
|
33526
|
+
depth: input.depth,
|
|
33527
|
+
keywordLimit: input.keywordLimit
|
|
33528
|
+
});
|
|
33529
|
+
const existing = db.prepare("SELECT * FROM rank_tracking_configs WHERE config_key = ?").get(configKey);
|
|
33530
|
+
if (existing) return configFromRow(existing);
|
|
33531
|
+
const total = db.prepare("SELECT COUNT(*) AS count FROM rank_tracking_configs").get().count;
|
|
33532
|
+
const projectTotal = db.prepare("SELECT COUNT(*) AS count FROM rank_tracking_configs WHERE project_id = ?").get(projectId).count;
|
|
33533
|
+
if (total >= RANK_TRACKING_LIMITS.totalConfigurations) invalid("Remove an unused rank tracking configuration before adding more.");
|
|
33534
|
+
if (projectTotal >= RANK_TRACKING_LIMITS.configurationsPerProject) invalid("This project already has the maximum rank tracking configurations.");
|
|
33535
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33536
|
+
const id = (options.id ?? randomUUID)();
|
|
33537
|
+
db.prepare(`INSERT INTO rank_tracking_configs
|
|
33538
|
+
(id, config_key, project_id, keyword_set_id, target_domain, tag,
|
|
33539
|
+
market_json, devices_json, provider, collection_method, cadence, depth,
|
|
33540
|
+
keyword_limit, next_run_at, created_at, updated_at)
|
|
33541
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, configKey, projectId, set.id, targetDomain, tag, JSON.stringify(marketWithoutDevice), JSON.stringify(devices), provider.data, input.collectionMethod, input.cadence, input.depth, input.keywordLimit, input.cadence === "manual" ? null : now, now, now);
|
|
33542
|
+
return configFromRow(db.prepare("SELECT * FROM rank_tracking_configs WHERE id = ?").get(id));
|
|
33543
|
+
}
|
|
33544
|
+
//#endregion
|
|
33545
|
+
//#region packages/core/src/rank-tracking/store.ts
|
|
33546
|
+
function activeRankTrackingRun(configId, options = {}) {
|
|
33547
|
+
const row = database(options).prepare(`SELECT * FROM rank_tracking_runs
|
|
33548
|
+
WHERE config_id = ? AND completed_at IS NULL
|
|
33549
|
+
ORDER BY scheduled_for DESC, id DESC LIMIT 1`).get(configId);
|
|
33550
|
+
return row ? runFromRow(row) : null;
|
|
33551
|
+
}
|
|
33552
|
+
function startRankTrackingRun(input, options = {}) {
|
|
33553
|
+
const db = database(options);
|
|
33554
|
+
const active = activeRankTrackingRun(input.configuration.id, { database: db });
|
|
33555
|
+
if (active) return active;
|
|
33556
|
+
if (input.keywords.length < 1) invalid("The selected keyword set view is empty.");
|
|
33557
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33558
|
+
const scheduledFor = input.scheduledFor?.getTime() ?? now;
|
|
33559
|
+
const runId = (options.id ?? randomUUID)();
|
|
33560
|
+
const idempotencyKey = `${input.configuration.id}:${scheduledFor}`;
|
|
33561
|
+
const taskCount = input.keywords.length * input.configuration.devices.length;
|
|
33562
|
+
const create = db.transaction(() => {
|
|
33563
|
+
db.prepare(`INSERT INTO rank_tracking_runs
|
|
33564
|
+
(id, config_id, idempotency_key, state, collection_method, scheduled_for,
|
|
33565
|
+
started_at, keyword_count, task_count, pending_count, config_snapshot_json)
|
|
33566
|
+
VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?)`).run(runId, input.configuration.id, idempotencyKey, input.configuration.collectionMethod, scheduledFor, now, input.keywords.length, taskCount, taskCount, JSON.stringify(input.configuration));
|
|
33567
|
+
const insertTask = db.prepare(`INSERT INTO rank_tracking_tasks
|
|
33568
|
+
(id, run_id, normalized_keyword, display_keyword, device, state)
|
|
33569
|
+
VALUES (?, ?, ?, ?, ?, 'pending')`);
|
|
33570
|
+
for (const keyword of input.keywords) for (const device of input.configuration.devices) insertTask.run((options.id ?? randomUUID)(), runId, keyword.normalizedKeyword, keyword.keyword, device);
|
|
33571
|
+
db.prepare(`UPDATE rank_tracking_configs
|
|
33572
|
+
SET next_run_at = ?, updated_at = ? WHERE id = ?`).run(nextRunAt(input.configuration.cadence, scheduledFor), now, input.configuration.id);
|
|
33573
|
+
});
|
|
33574
|
+
try {
|
|
33575
|
+
create.immediate();
|
|
33576
|
+
} catch (error) {
|
|
33577
|
+
if (/unique constraint/i.test(String(error))) {
|
|
33578
|
+
const recovered = activeRankTrackingRun(input.configuration.id, { database: db });
|
|
33579
|
+
if (recovered) return recovered;
|
|
33580
|
+
}
|
|
33581
|
+
throw error;
|
|
33582
|
+
}
|
|
33583
|
+
return getRankTrackingRun(runId, { database: db });
|
|
33584
|
+
}
|
|
33585
|
+
function getRankTrackingRun(runId, options = {}) {
|
|
33586
|
+
const row = database(options).prepare("SELECT * FROM rank_tracking_runs WHERE id = ?").get(runId);
|
|
33587
|
+
if (!row) throw new SeoError("INTERNAL_ERROR", "Rank tracking run is missing.");
|
|
33588
|
+
return runFromRow(row);
|
|
33589
|
+
}
|
|
33590
|
+
function rankTrackingTasks(runId, states, options = {}) {
|
|
33591
|
+
const filter = states?.length ? ` AND state IN (${states.map(() => "?").join(", ")})` : "";
|
|
33592
|
+
return database(options).prepare(`SELECT * FROM rank_tracking_tasks WHERE run_id = ?${filter}
|
|
33593
|
+
ORDER BY normalized_keyword, device, id`).all(runId, ...states ?? []).map(taskFromRow);
|
|
33594
|
+
}
|
|
33595
|
+
function markRankTasksPosted(runId, receipts, cost, options = {}) {
|
|
33596
|
+
const db = database(options);
|
|
33597
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33598
|
+
db.transaction(() => {
|
|
33599
|
+
const statement = db.prepare(`UPDATE rank_tracking_tasks
|
|
33600
|
+
SET state = 'posted', provider_task_id = ?, posted_at = ?,
|
|
33601
|
+
attempt_count = attempt_count + 1, error_code = NULL,
|
|
33602
|
+
error_message = NULL
|
|
33603
|
+
WHERE id = ? AND run_id = ? AND state IN ('pending', 'posting')`);
|
|
33604
|
+
for (const receipt of receipts) if (statement.run(receipt.providerTaskId, now, receipt.taskId, runId).changes !== 1) throw new SeoError("INTERNAL_ERROR", "A queued rank task changed before its receipt was saved.");
|
|
33605
|
+
db.prepare(`UPDATE rank_tracking_runs
|
|
33606
|
+
SET estimated_cost_micros = CASE
|
|
33607
|
+
WHEN ? IS NULL THEN estimated_cost_micros
|
|
33608
|
+
ELSE COALESCE(estimated_cost_micros, 0) + ? END,
|
|
33609
|
+
actual_cost_micros = CASE
|
|
33610
|
+
WHEN ? IS NULL THEN actual_cost_micros
|
|
33611
|
+
ELSE COALESCE(actual_cost_micros, 0) + ? END
|
|
33612
|
+
WHERE id = ?`).run(cost.estimatedMicros, cost.estimatedMicros, cost.actualMicros, cost.actualMicros, runId);
|
|
33613
|
+
}).immediate();
|
|
33614
|
+
}
|
|
33615
|
+
function markRankTasksPosting(runId, taskIds, options = {}) {
|
|
33616
|
+
if (taskIds.length === 0) return;
|
|
33617
|
+
const db = database(options);
|
|
33618
|
+
db.transaction(() => {
|
|
33619
|
+
const statement = db.prepare(`UPDATE rank_tracking_tasks
|
|
33620
|
+
SET state = 'posting', attempt_count = attempt_count + 1
|
|
33621
|
+
WHERE id = ? AND run_id = ? AND state = 'pending'`);
|
|
33622
|
+
for (const taskId of taskIds) if (statement.run(taskId, runId).changes !== 1) throw new SeoError("INTERNAL_ERROR", "A queued rank task changed before posting started.");
|
|
33623
|
+
}).immediate();
|
|
33624
|
+
}
|
|
33625
|
+
function recoverRankTaskReceipt(input, options = {}) {
|
|
33626
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33627
|
+
database(options).prepare(`UPDATE rank_tracking_tasks
|
|
33628
|
+
SET state = 'posted', provider_task_id = ?, posted_at = ?
|
|
33629
|
+
WHERE id = ? AND state = 'posting'`).run(input.providerTaskId, now, input.taskId);
|
|
33630
|
+
}
|
|
33631
|
+
function saveRankObservation(observation, options = {}) {
|
|
33632
|
+
saveRankObservations([observation], options);
|
|
33633
|
+
}
|
|
33634
|
+
function saveRankObservations(observations, options = {}) {
|
|
33635
|
+
if (observations.length === 0) return;
|
|
33636
|
+
const runId = observations[0]?.runId;
|
|
33637
|
+
if (!runId || observations.some((observation) => observation.runId !== runId)) throw new SeoError("INTERNAL_ERROR", "A rank observation batch must belong to one run.");
|
|
33638
|
+
const db = database(options);
|
|
33639
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33640
|
+
db.transaction(() => {
|
|
33641
|
+
const insertSnapshot = db.prepare(`INSERT OR IGNORE INTO rank_tracking_snapshots
|
|
33642
|
+
(task_id, run_id, normalized_keyword, display_keyword, device,
|
|
33643
|
+
observation_state, organic_position, absolute_position, ranking_url,
|
|
33644
|
+
observed_features_json, checked_at, provider, provider_task_id,
|
|
33645
|
+
requested_depth, returned_rows, retained_rows, invalid_rows,
|
|
33646
|
+
completeness, estimated_cost_micros, actual_cost_micros, warnings_json,
|
|
33647
|
+
created_at)
|
|
33648
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
33649
|
+
const completeTask = db.prepare(`UPDATE rank_tracking_tasks
|
|
33650
|
+
SET state = 'complete', collected_at = ?, error_code = NULL,
|
|
33651
|
+
error_message = NULL WHERE id = ?`);
|
|
33652
|
+
const addLiveCost = db.prepare(`UPDATE rank_tracking_runs
|
|
33653
|
+
SET estimated_cost_micros = CASE
|
|
33654
|
+
WHEN ? IS NULL THEN estimated_cost_micros
|
|
33655
|
+
ELSE COALESCE(estimated_cost_micros, 0) + ? END,
|
|
33656
|
+
actual_cost_micros = CASE
|
|
33657
|
+
WHEN ? IS NULL THEN actual_cost_micros
|
|
33658
|
+
ELSE COALESCE(actual_cost_micros, 0) + ? END
|
|
33659
|
+
WHERE id = ? AND collection_method = 'live'`);
|
|
33660
|
+
for (const observation of observations) {
|
|
33661
|
+
const inserted = insertSnapshot.run(observation.taskId, observation.runId, observation.normalizedKeyword, observation.keyword, observation.device, observation.state, observation.organicPosition, observation.absolutePosition, observation.rankingUrl, JSON.stringify(observation.observedFeatures), observation.checkedAt, observation.provider, observation.providerTaskId, observation.requestedDepth, observation.returnedRows, observation.retainedRows, observation.invalidRows, observation.completeness, observation.estimatedCostMicros, observation.actualCostMicros, JSON.stringify(observation.warnings), now);
|
|
33662
|
+
completeTask.run(now, observation.taskId);
|
|
33663
|
+
if (inserted.changes === 1) addLiveCost.run(observation.estimatedCostMicros, observation.estimatedCostMicros, observation.actualCostMicros, observation.actualCostMicros, observation.runId);
|
|
33664
|
+
}
|
|
33665
|
+
}).immediate();
|
|
33666
|
+
refreshRankTrackingRun(runId, {
|
|
33667
|
+
database: db,
|
|
33668
|
+
now: options.now
|
|
33669
|
+
});
|
|
33670
|
+
}
|
|
33671
|
+
function failRankTrackingTask(input, options = {}) {
|
|
33672
|
+
const db = database(options);
|
|
33673
|
+
const row = db.prepare("SELECT run_id FROM rank_tracking_tasks WHERE id = ?").get(input.taskId);
|
|
33674
|
+
if (!row) return;
|
|
33675
|
+
db.prepare(`UPDATE rank_tracking_tasks
|
|
33676
|
+
SET state = 'failed', attempt_count = attempt_count + 1,
|
|
33677
|
+
error_code = ?, error_message = ? WHERE id = ?`).run(input.code.slice(0, 100), input.message.slice(0, 1e3), input.taskId);
|
|
33678
|
+
refreshRankTrackingRun(row.run_id, {
|
|
33679
|
+
database: db,
|
|
33680
|
+
now: options.now
|
|
33681
|
+
});
|
|
33682
|
+
}
|
|
33683
|
+
function refreshRankTrackingRun(runId, options = {}) {
|
|
33684
|
+
const db = database(options);
|
|
33685
|
+
const counts = db.prepare(`SELECT
|
|
33686
|
+
SUM(CASE WHEN state = 'complete' THEN 1 ELSE 0 END) AS snapshots,
|
|
33687
|
+
SUM(CASE WHEN state IN ('pending', 'posting', 'posted') THEN 1 ELSE 0 END) AS pending,
|
|
33688
|
+
SUM(CASE WHEN state = 'failed' THEN 1 ELSE 0 END) AS failed
|
|
33689
|
+
FROM rank_tracking_tasks WHERE run_id = ?`).get(runId);
|
|
33690
|
+
const state = counts.pending > 0 ? counts.snapshots > 0 || counts.failed > 0 ? "partial" : "pending" : counts.failed > 0 ? counts.snapshots > 0 ? "partial" : "failed" : "complete";
|
|
33691
|
+
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33692
|
+
db.prepare(`UPDATE rank_tracking_runs
|
|
33693
|
+
SET state = ?, snapshot_count = ?, pending_count = ?, failed_count = ?,
|
|
33694
|
+
completed_at = CASE WHEN ? = 0 THEN ? ELSE NULL END,
|
|
33695
|
+
error_summary = CASE WHEN ? > 0 THEN ? ELSE NULL END
|
|
33696
|
+
WHERE id = ?`).run(state, counts.snapshots, counts.pending, counts.failed, counts.pending, now, counts.failed, `${counts.failed} rank task${counts.failed === 1 ? "" : "s"} failed.`, runId);
|
|
33697
|
+
if (counts.pending === 0) pruneRankTrackingRuns(runId, {
|
|
33698
|
+
...options,
|
|
33699
|
+
database: db
|
|
33700
|
+
});
|
|
33701
|
+
return getRankTrackingRun(runId, { database: db });
|
|
33702
|
+
}
|
|
33703
|
+
function pruneRankTrackingRuns(runId, options = {}) {
|
|
33704
|
+
const db = database(options);
|
|
33705
|
+
const row = db.prepare("SELECT config_id FROM rank_tracking_runs WHERE id = ?").get(runId);
|
|
33706
|
+
if (!row) return;
|
|
33707
|
+
db.prepare(`DELETE FROM rank_tracking_runs
|
|
33708
|
+
WHERE config_id = ? AND id IN (
|
|
33709
|
+
SELECT id FROM rank_tracking_runs
|
|
33710
|
+
WHERE config_id = ? AND completed_at IS NOT NULL
|
|
33711
|
+
ORDER BY scheduled_for DESC, id DESC LIMIT -1 OFFSET ?
|
|
33712
|
+
)`).run(row.config_id, row.config_id, RANK_TRACKING_LIMITS.retainedRunsPerConfiguration);
|
|
33713
|
+
const maxBytes = options.maxLogicalBytes ?? RANK_TRACKING_LIMITS.logicalBytes;
|
|
33714
|
+
while (rankTrackingLogicalBytes(db) > maxBytes) {
|
|
33715
|
+
const oldest = db.prepare(`SELECT id FROM rank_tracking_runs
|
|
33716
|
+
WHERE config_id = ? AND id <> ?
|
|
33717
|
+
AND completed_at IS NOT NULL
|
|
33718
|
+
ORDER BY scheduled_for, id LIMIT 1`).get(row.config_id, runId);
|
|
33719
|
+
if (!oldest) break;
|
|
33720
|
+
db.prepare("DELETE FROM rank_tracking_runs WHERE id = ?").run(oldest.id);
|
|
33721
|
+
}
|
|
33722
|
+
enforceRankTrackingLogicalBytes(db, maxBytes);
|
|
33723
|
+
}
|
|
33724
|
+
function latestRankTrackingRun(configId, options = {}) {
|
|
33725
|
+
const row = database(options).prepare(`SELECT * FROM rank_tracking_runs WHERE config_id = ?
|
|
33726
|
+
ORDER BY scheduled_for DESC, id DESC LIMIT 1`).get(configId);
|
|
33727
|
+
return row ? runFromRow(row) : null;
|
|
33728
|
+
}
|
|
33729
|
+
function priorComparableRankTrackingRun(configId, currentRunId, options = {}) {
|
|
33730
|
+
const row = database(options).prepare(`SELECT previous.* FROM rank_tracking_runs previous
|
|
33731
|
+
JOIN rank_tracking_runs current ON current.id = ?
|
|
33732
|
+
WHERE previous.config_id = ? AND previous.id <> current.id
|
|
33733
|
+
AND previous.scheduled_for < current.scheduled_for
|
|
33734
|
+
AND previous.snapshot_count > 0
|
|
33735
|
+
ORDER BY previous.scheduled_for DESC, previous.id DESC LIMIT 1`).get(currentRunId, configId);
|
|
33736
|
+
return row ? runFromRow(row) : null;
|
|
33737
|
+
}
|
|
33738
|
+
function rankObservations(runId, options = {}) {
|
|
33739
|
+
return database(options).prepare(`SELECT * FROM rank_tracking_snapshots WHERE run_id = ?
|
|
33740
|
+
ORDER BY normalized_keyword, device, task_id`).all(runId).map(observationFromRow);
|
|
33741
|
+
}
|
|
33742
|
+
function rankTrackingLogicalBytes(db = getDb()) {
|
|
33743
|
+
return db.prepare(`SELECT
|
|
33744
|
+
COALESCE((SELECT SUM(length(id) + length(config_key) + length(project_id) + length(keyword_set_id) + length(target_domain) + COALESCE(length(tag), 0) + length(market_json) + length(devices_json) + length(provider) + length(collection_method) + length(cadence)) FROM rank_tracking_configs), 0) +
|
|
33745
|
+
COALESCE((SELECT SUM(length(id) + length(config_id) + length(idempotency_key) + length(state) + length(collection_method) + length(config_snapshot_json) + COALESCE(length(error_summary), 0)) FROM rank_tracking_runs), 0) +
|
|
33746
|
+
COALESCE((SELECT SUM(length(id) + length(run_id) + length(normalized_keyword) + length(display_keyword) + length(device) + length(state) + COALESCE(length(provider_task_id), 0) + COALESCE(length(error_code), 0) + COALESCE(length(error_message), 0)) FROM rank_tracking_tasks), 0) +
|
|
33747
|
+
COALESCE((SELECT SUM(length(task_id) + length(run_id) + length(normalized_keyword) + length(display_keyword) + length(device) + length(observation_state) + COALESCE(length(ranking_url), 0) + length(observed_features_json) + length(checked_at) + length(provider) + COALESCE(length(provider_task_id), 0) + length(completeness) + length(warnings_json)) FROM rank_tracking_snapshots), 0)
|
|
33748
|
+
AS bytes`).get().bytes;
|
|
33749
|
+
}
|
|
33750
|
+
function enforceRankTrackingLogicalBytes(db, maxBytes = RANK_TRACKING_LIMITS.logicalBytes) {
|
|
33751
|
+
if (rankTrackingLogicalBytes(db) > maxBytes) throw new SeoError("INVALID_INPUT", "Saved rank history reached its local storage limit. Remove an unused tracking configuration before collecting more.");
|
|
33752
|
+
}
|
|
33753
|
+
//#endregion
|
|
33754
|
+
//#region packages/core/src/rank-tracking/run.ts
|
|
33755
|
+
function providerError$1(error) {
|
|
33756
|
+
if (!(error instanceof ProviderError)) return error instanceof SeoError ? error : new SeoError("INTERNAL_ERROR", "Rank collection failed unexpectedly.");
|
|
33757
|
+
if (error.code === "rate-limit") return new SeoError("RATE_LIMITED", error.message);
|
|
33758
|
+
if (error.code === "configuration") return new SeoError("INVALID_INPUT", error.message);
|
|
33759
|
+
return new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
33760
|
+
}
|
|
33761
|
+
async function defaultCollector(provider) {
|
|
33762
|
+
if (provider !== "dataforseo") throw new SeoError("PROVIDER_UNAVAILABLE", `${provider} does not yet implement exact rank collection. Use DataForSEO for this report.`);
|
|
33763
|
+
if (!await readDataForSeoCredentials()) throw new SeoError("PROVIDER_UNAVAILABLE", "DataForSEO is not connected. Run `seo providers dataforseo connect` first.");
|
|
33764
|
+
return new DataForSeoRankTrackingCollector();
|
|
33765
|
+
}
|
|
33766
|
+
function requestForTask(configuration, task) {
|
|
33767
|
+
return {
|
|
33768
|
+
keyword: task.displayKeyword,
|
|
33769
|
+
market: {
|
|
33770
|
+
...configuration.market,
|
|
33771
|
+
device: task.device
|
|
33772
|
+
},
|
|
33773
|
+
depth: configuration.depth,
|
|
33774
|
+
refresh: true,
|
|
33775
|
+
context: {
|
|
33776
|
+
projectId: configuration.projectId,
|
|
33777
|
+
reportId: "rank-tracking",
|
|
33778
|
+
reportRunId: task.runId
|
|
33779
|
+
}
|
|
33780
|
+
};
|
|
33781
|
+
}
|
|
33782
|
+
function observationFromEvidence(configuration, task, evidence) {
|
|
33783
|
+
const match = evidence.data.organicResults.find((result) => {
|
|
33784
|
+
try {
|
|
33785
|
+
return targetMatchesDomain(configuration.targetDomain, result.domain);
|
|
33786
|
+
} catch {
|
|
33787
|
+
return false;
|
|
33788
|
+
}
|
|
33789
|
+
});
|
|
33790
|
+
return {
|
|
33791
|
+
taskId: task.id,
|
|
33792
|
+
runId: task.runId,
|
|
33793
|
+
keyword: task.displayKeyword,
|
|
33794
|
+
normalizedKeyword: task.normalizedKeyword,
|
|
33795
|
+
device: task.device,
|
|
33796
|
+
state: match ? "observed" : "not_observed_within_depth",
|
|
33797
|
+
organicPosition: match?.rankGroup ?? null,
|
|
33798
|
+
absolutePosition: match?.rankAbsolute ?? null,
|
|
33799
|
+
rankingUrl: match?.url ?? null,
|
|
33800
|
+
observedFeatures: evidence.data.features,
|
|
33801
|
+
checkedAt: evidence.data.checkedAt,
|
|
33802
|
+
provider: evidence.provider,
|
|
33803
|
+
providerTaskId: task.providerTaskId,
|
|
33804
|
+
requestedDepth: configuration.depth,
|
|
33805
|
+
returnedRows: evidence.coverage.returnedRows,
|
|
33806
|
+
retainedRows: evidence.coverage.retainedRows,
|
|
33807
|
+
invalidRows: evidence.coverage.invalidRows,
|
|
33808
|
+
completeness: evidence.coverage.completeness,
|
|
33809
|
+
estimatedCostMicros: evidence.cost.estimatedMicros,
|
|
33810
|
+
actualCostMicros: evidence.cost.actualMicros,
|
|
33811
|
+
warnings: evidence.warnings
|
|
33812
|
+
};
|
|
33813
|
+
}
|
|
33814
|
+
async function inBoundedParallel(items, concurrency, operation) {
|
|
33815
|
+
let next = 0;
|
|
33816
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
33817
|
+
while (next < items.length) {
|
|
33818
|
+
const index = next;
|
|
33819
|
+
next += 1;
|
|
33820
|
+
const item = items[index];
|
|
33821
|
+
if (item) await operation(item);
|
|
33822
|
+
}
|
|
33823
|
+
});
|
|
33824
|
+
await Promise.all(workers);
|
|
33825
|
+
}
|
|
33826
|
+
async function collectLive(configuration, run, collector, dependencies) {
|
|
33827
|
+
const tasks = rankTrackingTasks(run.id, ["pending"], dependencies);
|
|
33828
|
+
const observations = [];
|
|
33829
|
+
const warnings = [];
|
|
33830
|
+
await inBoundedParallel(tasks, 3, async (task) => {
|
|
33831
|
+
try {
|
|
33832
|
+
const evidence = await collector.live(requestForTask(configuration, task));
|
|
33833
|
+
observations.push(observationFromEvidence(configuration, task, evidence));
|
|
33834
|
+
} catch (error) {
|
|
33835
|
+
const mapped = providerError$1(error);
|
|
33836
|
+
failRankTrackingTask({
|
|
33837
|
+
taskId: task.id,
|
|
33838
|
+
code: mapped.code,
|
|
33839
|
+
message: mapped.message
|
|
33840
|
+
}, dependencies);
|
|
33841
|
+
warnings.push(`${task.displayKeyword} (${task.device}): ${mapped.message}`);
|
|
33842
|
+
}
|
|
33843
|
+
});
|
|
33844
|
+
saveRankObservations(observations, dependencies);
|
|
33845
|
+
return {
|
|
33846
|
+
collected: observations.length,
|
|
33847
|
+
warnings
|
|
33848
|
+
};
|
|
33849
|
+
}
|
|
33850
|
+
async function collectQueued(configuration, run, collector, dependencies) {
|
|
33851
|
+
if (!collector.post || !collector.ready || !collector.collect) throw new SeoError("PROVIDER_UNAVAILABLE", `${collector.provider} does not support queued rank collection. Use live collection or another provider.`);
|
|
33852
|
+
const warnings = [];
|
|
33853
|
+
const observations = [];
|
|
33854
|
+
let posted = 0;
|
|
33855
|
+
let ready = [];
|
|
33856
|
+
try {
|
|
33857
|
+
ready = await collector.ready();
|
|
33858
|
+
} catch (error) {
|
|
33859
|
+
const mapped = providerError$1(error);
|
|
33860
|
+
warnings.push(`The provider ready list was unavailable; saved task IDs will still be checked: ${mapped.message}`);
|
|
33861
|
+
}
|
|
33862
|
+
const byTaskKey = new Map(ready.flatMap((item) => item.taskKey ? [[item.taskKey, item.providerTaskId]] : []));
|
|
33863
|
+
for (const task of rankTrackingTasks(run.id, ["posting"], dependencies)) {
|
|
33864
|
+
const providerTaskId = byTaskKey.get(task.id);
|
|
33865
|
+
if (providerTaskId) recoverRankTaskReceipt({
|
|
33866
|
+
taskId: task.id,
|
|
33867
|
+
providerTaskId
|
|
33868
|
+
}, dependencies);
|
|
33869
|
+
}
|
|
33870
|
+
await inBoundedParallel(rankTrackingTasks(run.id, ["posted"], dependencies).filter((task) => task.providerTaskId), 5, async (task) => {
|
|
33871
|
+
try {
|
|
33872
|
+
const evidence = await collector.collect?.({
|
|
33873
|
+
providerTaskId: task.providerTaskId,
|
|
33874
|
+
request: requestForTask(configuration, task)
|
|
33875
|
+
});
|
|
33876
|
+
if (!evidence) return;
|
|
33877
|
+
observations.push(observationFromEvidence(configuration, task, evidence));
|
|
33878
|
+
} catch (error) {
|
|
33879
|
+
const mapped = providerError$1(error);
|
|
33880
|
+
warnings.push(`${task.displayKeyword} (${task.device}): ${mapped.message}`);
|
|
33881
|
+
}
|
|
33882
|
+
});
|
|
33883
|
+
saveRankObservations(observations, dependencies);
|
|
33884
|
+
const pending = rankTrackingTasks(run.id, ["pending"], dependencies);
|
|
33885
|
+
for (let offset = 0; offset < pending.length; offset += RANK_TRACKING_LIMITS.tasksPerProviderPost) {
|
|
33886
|
+
const batch = pending.slice(offset, offset + RANK_TRACKING_LIMITS.tasksPerProviderPost);
|
|
33887
|
+
markRankTasksPosting(run.id, batch.map((task) => task.id), dependencies);
|
|
33888
|
+
try {
|
|
33889
|
+
const result = await collector.post({
|
|
33890
|
+
tasks: batch.map((task) => ({
|
|
33891
|
+
taskKey: task.id,
|
|
33892
|
+
request: requestForTask(configuration, task)
|
|
33893
|
+
})),
|
|
33894
|
+
context: {
|
|
33895
|
+
projectId: configuration.projectId,
|
|
33896
|
+
reportId: "rank-tracking",
|
|
33897
|
+
reportRunId: run.id
|
|
33898
|
+
}
|
|
33899
|
+
});
|
|
33900
|
+
const receiptByKey = new Map(result.receipts.map((receipt) => [receipt.taskKey, receipt]));
|
|
33901
|
+
if (receiptByKey.size !== batch.length) throw new SeoError("PROVIDER_UNAVAILABLE", "The provider did not return one receipt for every queued rank task.");
|
|
33902
|
+
markRankTasksPosted(run.id, batch.map((task) => ({
|
|
33903
|
+
taskId: task.id,
|
|
33904
|
+
providerTaskId: receiptByKey.get(task.id)?.providerTaskId ?? ""
|
|
33905
|
+
})), {
|
|
33906
|
+
estimatedMicros: result.estimatedCostMicros,
|
|
33907
|
+
actualMicros: result.actualCostMicros
|
|
33908
|
+
}, dependencies);
|
|
33909
|
+
posted += batch.length;
|
|
33910
|
+
warnings.push(...result.warnings.map((warning) => warning.message));
|
|
33911
|
+
} catch (error) {
|
|
33912
|
+
const mapped = providerError$1(error);
|
|
33913
|
+
warnings.push(`${batch.length} queued task${batch.length === 1 ? "" : "s"} remain in recovery after posting was interrupted: ${mapped.message}`);
|
|
33914
|
+
break;
|
|
33915
|
+
}
|
|
33916
|
+
}
|
|
33917
|
+
return {
|
|
33918
|
+
collected: observations.length,
|
|
33919
|
+
posted,
|
|
33920
|
+
warnings
|
|
33921
|
+
};
|
|
33922
|
+
}
|
|
33923
|
+
function due(configuration, now) {
|
|
33924
|
+
return configuration.cadence === "manual" || configuration.nextRunAt === null || Date.parse(configuration.nextRunAt) <= now.getTime();
|
|
33925
|
+
}
|
|
33926
|
+
async function executeRankTracking(input, dependencies = {}) {
|
|
33927
|
+
const cadence = input.cadence ?? "manual";
|
|
33928
|
+
const collectionMethod = input.collectionMethod ?? (cadence === "manual" ? "live" : "queued");
|
|
33929
|
+
const keywordLimit = input.keywordLimit ?? (collectionMethod === "live" ? 25 : 100);
|
|
33930
|
+
const set = getKeywordSet({
|
|
33931
|
+
projectId: input.projectId,
|
|
33932
|
+
idOrName: input.set,
|
|
33933
|
+
tag: input.tag,
|
|
33934
|
+
limit: keywordLimit
|
|
33935
|
+
}, dependencies);
|
|
33936
|
+
const provider = input.provider ?? set.set.provider ?? "dataforseo";
|
|
33937
|
+
const defaultDevice = set.set.market.device ?? "desktop";
|
|
33938
|
+
const configuration = getOrCreateRankTrackingConfiguration({
|
|
33939
|
+
projectId: input.projectId,
|
|
33940
|
+
keywordSetId: set.set.id,
|
|
33941
|
+
targetDomain: input.targetDomain,
|
|
33942
|
+
tag: input.tag,
|
|
33943
|
+
market: set.set.market,
|
|
33944
|
+
devices: input.devices ?? [defaultDevice],
|
|
33945
|
+
provider,
|
|
33946
|
+
collectionMethod,
|
|
33947
|
+
cadence,
|
|
33948
|
+
depth: input.depth ?? 100,
|
|
33949
|
+
keywordLimit
|
|
33950
|
+
}, dependencies);
|
|
33951
|
+
const warnings = [];
|
|
33952
|
+
let run = activeRankTrackingRun(configuration.id, dependencies);
|
|
33953
|
+
let startedRun = false;
|
|
33954
|
+
let collectedTasks = 0;
|
|
33955
|
+
let postedTasks = 0;
|
|
33956
|
+
if (!run && (input.start ?? true) && due(configuration, dependencies.now?.() ?? /* @__PURE__ */ new Date())) {
|
|
33957
|
+
run = startRankTrackingRun({
|
|
33958
|
+
configuration,
|
|
33959
|
+
keywords: set.items.map((item) => ({
|
|
33960
|
+
keyword: item.keyword,
|
|
33961
|
+
normalizedKeyword: item.normalizedKeyword
|
|
33962
|
+
}))
|
|
33963
|
+
}, {
|
|
33964
|
+
...dependencies,
|
|
33965
|
+
id: dependencies.id ?? randomUUID
|
|
33966
|
+
});
|
|
33967
|
+
startedRun = true;
|
|
33968
|
+
}
|
|
33969
|
+
if (run) {
|
|
33970
|
+
const collector = dependencies.collector ?? await (dependencies.collectorFactory ?? defaultCollector)(provider);
|
|
33971
|
+
if (collector.provider !== provider) throw new SeoError("INTERNAL_ERROR", "The selected rank collector does not match the saved provider.");
|
|
33972
|
+
if (configuration.collectionMethod === "live") {
|
|
33973
|
+
const result = await collectLive(configuration, run, collector, dependencies);
|
|
33974
|
+
collectedTasks += result.collected;
|
|
33975
|
+
warnings.push(...result.warnings);
|
|
33976
|
+
} else {
|
|
33977
|
+
const result = await collectQueued(configuration, run, collector, dependencies);
|
|
33978
|
+
collectedTasks += result.collected;
|
|
33979
|
+
postedTasks += result.posted;
|
|
33980
|
+
warnings.push(...result.warnings);
|
|
33981
|
+
}
|
|
33982
|
+
run = latestRankTrackingRun(configuration.id, dependencies);
|
|
33983
|
+
} else run = latestRankTrackingRun(configuration.id, dependencies);
|
|
33984
|
+
return {
|
|
33985
|
+
configuration,
|
|
33986
|
+
run,
|
|
33987
|
+
startedRun,
|
|
33988
|
+
collectedTasks,
|
|
33989
|
+
postedTasks,
|
|
33990
|
+
operationalWarnings: warnings.slice(0, 20)
|
|
33991
|
+
};
|
|
33992
|
+
}
|
|
33993
|
+
//#endregion
|
|
33994
|
+
//#region packages/core/src/analyze/rank-tracking.ts
|
|
33995
|
+
function key$1(item) {
|
|
33996
|
+
return `${item.normalizedKeyword}\u0000${item.device}`;
|
|
33997
|
+
}
|
|
33998
|
+
function changeFor(current, previous) {
|
|
33999
|
+
if (!previous) return {
|
|
34000
|
+
change: "no-history",
|
|
34001
|
+
organicPositionDelta: null,
|
|
34002
|
+
rankingUrlChanged: false
|
|
34003
|
+
};
|
|
34004
|
+
const rankingUrlChanged = current.state === "observed" && previous.state === "observed" && current.rankingUrl !== previous.rankingUrl;
|
|
34005
|
+
if (current.state === "observed" && previous.state === "not_observed_within_depth") return {
|
|
34006
|
+
change: "new",
|
|
34007
|
+
organicPositionDelta: null,
|
|
34008
|
+
rankingUrlChanged
|
|
34009
|
+
};
|
|
34010
|
+
if (current.state === "not_observed_within_depth" && previous.state === "observed") return {
|
|
34011
|
+
change: "lost",
|
|
34012
|
+
organicPositionDelta: null,
|
|
34013
|
+
rankingUrlChanged
|
|
34014
|
+
};
|
|
34015
|
+
if (current.organicPosition !== null && previous.organicPosition !== null) {
|
|
34016
|
+
const delta = previous.organicPosition - current.organicPosition;
|
|
34017
|
+
return {
|
|
34018
|
+
change: delta > 0 ? "improved" : delta < 0 ? "declined" : "unchanged",
|
|
34019
|
+
organicPositionDelta: delta,
|
|
34020
|
+
rankingUrlChanged
|
|
34021
|
+
};
|
|
34022
|
+
}
|
|
34023
|
+
return {
|
|
34024
|
+
change: "unchanged",
|
|
34025
|
+
organicPositionDelta: null,
|
|
34026
|
+
rankingUrlChanged
|
|
34027
|
+
};
|
|
34028
|
+
}
|
|
34029
|
+
const CHANGE_ORDER = {
|
|
34030
|
+
lost: 0,
|
|
34031
|
+
declined: 1,
|
|
34032
|
+
new: 2,
|
|
34033
|
+
improved: 3,
|
|
34034
|
+
unchanged: 4,
|
|
34035
|
+
"no-history": 5
|
|
34036
|
+
};
|
|
34037
|
+
function compareItems(left, right) {
|
|
34038
|
+
return CHANGE_ORDER[left.change] - CHANGE_ORDER[right.change] || Math.abs(right.organicPositionDelta ?? 0) - Math.abs(left.organicPositionDelta ?? 0) || (left.normalizedKeyword < right.normalizedKeyword ? -1 : left.normalizedKeyword > right.normalizedKeyword ? 1 : left.device < right.device ? -1 : left.device > right.device ? 1 : 0);
|
|
34039
|
+
}
|
|
34040
|
+
function reportItems(current, previous, pending) {
|
|
34041
|
+
const previousByKey = new Map(previous.map((item) => [key$1(item), item]));
|
|
34042
|
+
const items = current.map((item) => {
|
|
34043
|
+
const prior = previousByKey.get(key$1(item));
|
|
34044
|
+
const change = changeFor(item, prior);
|
|
34045
|
+
return {
|
|
34046
|
+
keyword: item.keyword,
|
|
34047
|
+
normalizedKeyword: item.normalizedKeyword,
|
|
34048
|
+
device: item.device,
|
|
34049
|
+
state: item.state,
|
|
34050
|
+
organicPosition: item.organicPosition,
|
|
34051
|
+
absolutePosition: item.absolutePosition,
|
|
34052
|
+
rankingUrl: item.rankingUrl,
|
|
34053
|
+
checkedAt: item.checkedAt,
|
|
34054
|
+
previous: prior ? {
|
|
34055
|
+
state: prior.state,
|
|
34056
|
+
organicPosition: prior.organicPosition,
|
|
34057
|
+
absolutePosition: prior.absolutePosition,
|
|
34058
|
+
rankingUrl: prior.rankingUrl,
|
|
34059
|
+
checkedAt: prior.checkedAt
|
|
34060
|
+
} : null,
|
|
34061
|
+
...change,
|
|
34062
|
+
error: null
|
|
34063
|
+
};
|
|
34064
|
+
});
|
|
34065
|
+
const observedKeys = new Set(current.map(key$1));
|
|
34066
|
+
for (const task of pending) {
|
|
34067
|
+
if (observedKeys.has(key$1(task))) continue;
|
|
34068
|
+
const prior = previousByKey.get(key$1(task));
|
|
34069
|
+
items.push({
|
|
34070
|
+
keyword: task.displayKeyword,
|
|
34071
|
+
normalizedKeyword: task.normalizedKeyword,
|
|
34072
|
+
device: task.device,
|
|
34073
|
+
state: task.state === "failed" ? "failed" : "pending",
|
|
34074
|
+
organicPosition: null,
|
|
34075
|
+
absolutePosition: null,
|
|
34076
|
+
rankingUrl: null,
|
|
34077
|
+
checkedAt: null,
|
|
34078
|
+
previous: prior ? {
|
|
34079
|
+
state: prior.state,
|
|
34080
|
+
organicPosition: prior.organicPosition,
|
|
34081
|
+
absolutePosition: prior.absolutePosition,
|
|
34082
|
+
rankingUrl: prior.rankingUrl,
|
|
34083
|
+
checkedAt: prior.checkedAt
|
|
34084
|
+
} : null,
|
|
34085
|
+
change: "no-history",
|
|
34086
|
+
organicPositionDelta: null,
|
|
34087
|
+
rankingUrlChanged: false,
|
|
34088
|
+
error: task.state === "failed" ? {
|
|
34089
|
+
code: task.errorCode,
|
|
34090
|
+
message: task.errorMessage
|
|
34091
|
+
} : null
|
|
34092
|
+
});
|
|
34093
|
+
}
|
|
34094
|
+
return items.sort(compareItems);
|
|
34095
|
+
}
|
|
34096
|
+
function verdict$1(run, counts) {
|
|
34097
|
+
if (!run) return "No rank tracking run has started for this configuration.";
|
|
34098
|
+
if (run.pendingCount > 0) return `${run.snapshotCount} of ${run.taskCount} market and device-specific snapshots are collected; ${run.pendingCount} remain pending.`;
|
|
34099
|
+
if (run.failedCount > 0) return `${run.snapshotCount} snapshots were collected and ${run.failedCount} failed; use the completed rows as partial evidence.`;
|
|
34100
|
+
if (counts.improved + counts.declined + counts.newCount + counts.lost > 0) return `${counts.improved} improved, ${counts.declined} declined, ${counts.newCount} became observed, and ${counts.lost} were no longer observed within the tracked depth.`;
|
|
34101
|
+
return `${counts.observed} rankings were observed and ${counts.notObserved} were not observed within the tracked depth.`;
|
|
34102
|
+
}
|
|
34103
|
+
async function rankTrackingReport(input, dependencies = {}) {
|
|
34104
|
+
const outputLimit = input.outputLimit ?? 100;
|
|
34105
|
+
if (!Number.isSafeInteger(outputLimit) || outputLimit < 1 || outputLimit > 250) throw new SeoError("INVALID_INPUT", "Rank tracking output limit must be from 1 to 250.");
|
|
34106
|
+
const execution = await executeRankTracking(input, dependencies);
|
|
34107
|
+
const { configuration, run } = execution;
|
|
34108
|
+
const current = run ? rankObservations(run.id, dependencies) : [];
|
|
34109
|
+
const priorRun = run ? priorComparableRankTrackingRun(configuration.id, run.id, dependencies) : null;
|
|
34110
|
+
const allItems = reportItems(current, priorRun ? rankObservations(priorRun.id, dependencies) : [], run ? rankTrackingTasks(run.id, void 0, dependencies) : []);
|
|
34111
|
+
const count = (change) => allItems.filter((item) => item.change === change).length;
|
|
34112
|
+
const observed = current.filter((item) => item.state === "observed").length;
|
|
34113
|
+
const notObserved = current.length - observed;
|
|
34114
|
+
const improved = count("improved");
|
|
34115
|
+
const declined = count("declined");
|
|
34116
|
+
const newCount = count("new");
|
|
34117
|
+
const lost = count("lost");
|
|
34118
|
+
const unchanged = count("unchanged");
|
|
34119
|
+
const rankingUrlChanges = allItems.filter((item) => item.rankingUrlChanged).length;
|
|
34120
|
+
const items = allItems.slice(0, outputLimit);
|
|
34121
|
+
const dataStatus = !run ? "unavailable" : run.state === "complete" ? "complete" : run.state === "pending" ? "pending" : "partial";
|
|
34122
|
+
return {
|
|
34123
|
+
schemaVersion: 1,
|
|
34124
|
+
generatedAt: (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
34125
|
+
dataStatus,
|
|
34126
|
+
configuration,
|
|
34127
|
+
run,
|
|
34128
|
+
comparison: {
|
|
34129
|
+
priorRunId: priorRun?.id ?? null,
|
|
34130
|
+
priorStartedAt: priorRun?.startedAt ?? null,
|
|
34131
|
+
comparableItems: allItems.filter((item) => item.previous !== null).length
|
|
34132
|
+
},
|
|
34133
|
+
summary: {
|
|
34134
|
+
trackedKeywords: run?.keywordCount ?? 0,
|
|
34135
|
+
trackedDevices: configuration.devices.length,
|
|
34136
|
+
expectedSnapshots: run?.taskCount ?? 0,
|
|
34137
|
+
completedSnapshots: run?.snapshotCount ?? 0,
|
|
34138
|
+
pendingSnapshots: run?.pendingCount ?? 0,
|
|
34139
|
+
failedSnapshots: run?.failedCount ?? 0,
|
|
34140
|
+
observed,
|
|
34141
|
+
notObservedWithinDepth: notObserved,
|
|
34142
|
+
new: newCount,
|
|
34143
|
+
lost,
|
|
34144
|
+
improved,
|
|
34145
|
+
declined,
|
|
34146
|
+
unchanged,
|
|
34147
|
+
rankingUrlChanges,
|
|
34148
|
+
verdict: verdict$1(run, {
|
|
34149
|
+
observed,
|
|
34150
|
+
notObserved,
|
|
34151
|
+
improved,
|
|
34152
|
+
declined,
|
|
34153
|
+
newCount,
|
|
34154
|
+
lost
|
|
34155
|
+
})
|
|
34156
|
+
},
|
|
34157
|
+
items,
|
|
34158
|
+
coverage: {
|
|
34159
|
+
totalItems: allItems.length,
|
|
34160
|
+
returnedItems: items.length,
|
|
34161
|
+
outputLimit,
|
|
34162
|
+
omittedItems: allItems.length - items.length,
|
|
34163
|
+
requestedDepth: configuration.depth,
|
|
34164
|
+
collectionMethod: configuration.collectionMethod,
|
|
34165
|
+
runState: run?.state ?? "not-started"
|
|
34166
|
+
},
|
|
34167
|
+
cost: {
|
|
34168
|
+
currency: "USD",
|
|
34169
|
+
estimatedMicros: run?.estimatedCostMicros ?? null,
|
|
34170
|
+
actualMicros: run?.actualCostMicros ?? null
|
|
34171
|
+
},
|
|
34172
|
+
operationalWarnings: execution.operationalWarnings,
|
|
34173
|
+
caveats: [
|
|
34174
|
+
`Not observed means the target domain was not retained within the first ${configuration.depth} organic results for that exact market and device. It is not proof that the domain does not rank elsewhere.`,
|
|
34175
|
+
"Exact snapshots and Search Console average position answer different questions and should not be substituted for one another.",
|
|
34176
|
+
"Local, national, desktop, and mobile snapshots remain separate configurations and are never blended into one position.",
|
|
34177
|
+
"Search results can change between checks and may differ from signed-in or personalized results.",
|
|
34178
|
+
...run?.state === "partial" || run?.state === "failed" ? ["Failed or pending tasks make this run partial; do not treat missing rows as losses."] : []
|
|
34179
|
+
],
|
|
34180
|
+
nextSteps: [
|
|
34181
|
+
...run?.pendingCount ? ["Run the same report parameters again after queued tasks are ready; collection is free and resumes from saved task IDs."] : [],
|
|
34182
|
+
"Use Search Console opportunity reports to add clicks, impressions, and average-position context before prioritising a ranking change.",
|
|
34183
|
+
"Inspect ranking URL changes for intent drift or competing templates before changing content.",
|
|
34184
|
+
configuration.cadence === "manual" ? "Use queued collection with a daily, weekly, or monthly cadence when this set needs recurring local tracking." : "Run this report from local cron at the configured cadence; overlapping runs are refused and schedule advancement is deterministic."
|
|
34185
|
+
]
|
|
34186
|
+
};
|
|
34187
|
+
}
|
|
34188
|
+
//#endregion
|
|
32553
34189
|
//#region packages/core/src/analyze/reports/dates.ts
|
|
32554
34190
|
function rangeDays(range) {
|
|
32555
34191
|
const start = Date.parse(`${range.startDate}T00:00:00Z`);
|
|
@@ -33015,210 +34651,6 @@ function reportPresentation(report) {
|
|
|
33015
34651
|
};
|
|
33016
34652
|
}
|
|
33017
34653
|
//#endregion
|
|
33018
|
-
//#region packages/core/src/keyword-sets/limits.ts
|
|
33019
|
-
const KEYWORD_SET_LIMITS = {
|
|
33020
|
-
setsPerProject: 25,
|
|
33021
|
-
totalSets: 100,
|
|
33022
|
-
keywordsPerSet: 1e4,
|
|
33023
|
-
totalKeywords: 1e5,
|
|
33024
|
-
mutationKeywords: 1e3,
|
|
33025
|
-
tagsPerKeyword: 20,
|
|
33026
|
-
outputRows: 1e3,
|
|
33027
|
-
logicalBytes: 64 * 1024 * 1024
|
|
33028
|
-
};
|
|
33029
|
-
const KEYWORD_SET_FIELD_LIMITS = {
|
|
33030
|
-
name: 80,
|
|
33031
|
-
projectId: 80,
|
|
33032
|
-
sourceReport: 100,
|
|
33033
|
-
keyword: 80,
|
|
33034
|
-
keywordWords: 10,
|
|
33035
|
-
tag: 40,
|
|
33036
|
-
url: 2048,
|
|
33037
|
-
metricJsonBytes: 16 * 1024
|
|
33038
|
-
};
|
|
33039
|
-
//#endregion
|
|
33040
|
-
//#region packages/core/src/keyword-sets/validation.ts
|
|
33041
|
-
const unavailableValueSchema = z.strictObject({
|
|
33042
|
-
state: z.enum([
|
|
33043
|
-
"missing",
|
|
33044
|
-
"unavailable",
|
|
33045
|
-
"invalid"
|
|
33046
|
-
]),
|
|
33047
|
-
value: z.null(),
|
|
33048
|
-
reason: z.string().min(1).max(500)
|
|
33049
|
-
});
|
|
33050
|
-
const providerValueNumberSchema = z.discriminatedUnion("state", [z.strictObject({
|
|
33051
|
-
state: z.literal("observed"),
|
|
33052
|
-
value: z.number().finite()
|
|
33053
|
-
}), unavailableValueSchema]);
|
|
33054
|
-
const providerValueStringSchema = z.discriminatedUnion("state", [z.strictObject({
|
|
33055
|
-
state: z.literal("observed"),
|
|
33056
|
-
value: z.string().max(500)
|
|
33057
|
-
}), unavailableValueSchema]);
|
|
33058
|
-
const monthlySearchSchema = z.strictObject({
|
|
33059
|
-
year: z.number().int().min(2e3).max(2200),
|
|
33060
|
-
month: z.number().int().min(1).max(12),
|
|
33061
|
-
searchVolume: z.number().finite().nonnegative()
|
|
33062
|
-
});
|
|
33063
|
-
const providerValueMonthlySearchesSchema = z.discriminatedUnion("state", [z.strictObject({
|
|
33064
|
-
state: z.literal("observed"),
|
|
33065
|
-
value: z.array(monthlySearchSchema).max(24)
|
|
33066
|
-
}), unavailableValueSchema]);
|
|
33067
|
-
const keywordMetricSchema = z.strictObject({
|
|
33068
|
-
keyword: z.string().min(1).max(KEYWORD_SET_FIELD_LIMITS.keyword),
|
|
33069
|
-
monthlySearchVolume: providerValueNumberSchema,
|
|
33070
|
-
monthlySearches: providerValueMonthlySearchesSchema,
|
|
33071
|
-
searchVolumeUpdatedAt: providerValueStringSchema,
|
|
33072
|
-
cpcUsd: providerValueNumberSchema,
|
|
33073
|
-
paidCompetition: providerValueNumberSchema,
|
|
33074
|
-
keywordDifficulty: providerValueNumberSchema,
|
|
33075
|
-
intent: providerValueStringSchema,
|
|
33076
|
-
resultCount: providerValueNumberSchema
|
|
33077
|
-
});
|
|
33078
|
-
const savedMetricSchema = z.strictObject({
|
|
33079
|
-
schemaVersion: z.literal(1),
|
|
33080
|
-
provider: providerIdSchema,
|
|
33081
|
-
observedAt: z.iso.datetime(),
|
|
33082
|
-
metric: keywordMetricSchema
|
|
33083
|
-
});
|
|
33084
|
-
function invalid(message) {
|
|
33085
|
-
throw new SeoError("INVALID_INPUT", message);
|
|
33086
|
-
}
|
|
33087
|
-
function boundedText(value, label, max) {
|
|
33088
|
-
const normalized = value.trim();
|
|
33089
|
-
if (!normalized || normalized.length > max) invalid(`${label} must contain 1 to ${max} characters.`);
|
|
33090
|
-
return normalized;
|
|
33091
|
-
}
|
|
33092
|
-
function normalizeSavedKeyword(value) {
|
|
33093
|
-
const normalized = value.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
33094
|
-
if (!normalized || normalized.length > KEYWORD_SET_FIELD_LIMITS.keyword || normalized.split(" ").length > KEYWORD_SET_FIELD_LIMITS.keywordWords) invalid(`Keywords must contain 1 to ${KEYWORD_SET_FIELD_LIMITS.keyword} characters and at most ${KEYWORD_SET_FIELD_LIMITS.keywordWords} words.`);
|
|
33095
|
-
return normalized;
|
|
33096
|
-
}
|
|
33097
|
-
function displayKeyword(value) {
|
|
33098
|
-
normalizeSavedKeyword(value);
|
|
33099
|
-
return value.trim().replace(/\s+/gu, " ");
|
|
33100
|
-
}
|
|
33101
|
-
function normalizedTags(values = []) {
|
|
33102
|
-
if (values.length > KEYWORD_SET_LIMITS.tagsPerKeyword) invalid(`A keyword can have at most ${KEYWORD_SET_LIMITS.tagsPerKeyword} tags.`);
|
|
33103
|
-
const tags = [...new Set(values.map((value) => boundedText(value, "Tag", KEYWORD_SET_FIELD_LIMITS.tag).toLowerCase()))].sort();
|
|
33104
|
-
if (tags.length > KEYWORD_SET_LIMITS.tagsPerKeyword) invalid(`A keyword can have at most ${KEYWORD_SET_LIMITS.tagsPerKeyword} tags.`);
|
|
33105
|
-
return tags;
|
|
33106
|
-
}
|
|
33107
|
-
function pageMapping(value) {
|
|
33108
|
-
if (value === null || value === void 0) return value;
|
|
33109
|
-
if (value.kind !== "target" && value.kind !== "proposed") invalid("Page mapping kind must be target or proposed.");
|
|
33110
|
-
const raw = boundedText(value.url, "Page URL", KEYWORD_SET_FIELD_LIMITS.url);
|
|
33111
|
-
let url;
|
|
33112
|
-
try {
|
|
33113
|
-
url = new URL(raw);
|
|
33114
|
-
} catch {
|
|
33115
|
-
invalid("Page mapping must use an absolute HTTP or HTTPS URL.");
|
|
33116
|
-
}
|
|
33117
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") invalid("Page mapping must use an absolute HTTP or HTTPS URL.");
|
|
33118
|
-
return {
|
|
33119
|
-
kind: value.kind,
|
|
33120
|
-
url: url.toString()
|
|
33121
|
-
};
|
|
33122
|
-
}
|
|
33123
|
-
function savedMetric(value, normalizedKeyword) {
|
|
33124
|
-
if (value === null || value === void 0) return {
|
|
33125
|
-
value,
|
|
33126
|
-
json: null
|
|
33127
|
-
};
|
|
33128
|
-
const parsed = savedMetricSchema.safeParse(value);
|
|
33129
|
-
if (!parsed.success) invalid("Use a valid normalized keyword metric snapshot.");
|
|
33130
|
-
if (normalizeSavedKeyword(parsed.data.metric.keyword) !== normalizedKeyword) invalid("A saved metric must describe the same keyword as the set item.");
|
|
33131
|
-
const json = JSON.stringify(parsed.data);
|
|
33132
|
-
if (Buffer.byteLength(json) > KEYWORD_SET_FIELD_LIMITS.metricJsonBytes) invalid(`A saved keyword metric cannot exceed ${KEYWORD_SET_FIELD_LIMITS.metricJsonBytes} bytes.`);
|
|
33133
|
-
return {
|
|
33134
|
-
value: parsed.data,
|
|
33135
|
-
json
|
|
33136
|
-
};
|
|
33137
|
-
}
|
|
33138
|
-
function validateMutationItems(items) {
|
|
33139
|
-
if (items.length < 1 || items.length > KEYWORD_SET_LIMITS.mutationKeywords) invalid(`Add 1 to ${KEYWORD_SET_LIMITS.mutationKeywords} keywords per operation.`);
|
|
33140
|
-
const normalized = /* @__PURE__ */ new Map();
|
|
33141
|
-
for (const item of items) {
|
|
33142
|
-
const key = normalizeSavedKeyword(item.keyword);
|
|
33143
|
-
const existing = normalized.get(key);
|
|
33144
|
-
const tags = normalizedTags([...existing?.tags ?? [], ...item.tags ?? []]);
|
|
33145
|
-
const page = pageMapping(item.page !== void 0 ? item.page : existing?.page);
|
|
33146
|
-
const metric = savedMetric(item.latestMetric !== void 0 ? item.latestMetric : existing?.metric, key);
|
|
33147
|
-
normalized.set(key, {
|
|
33148
|
-
keyword: existing?.keyword ?? displayKeyword(item.keyword),
|
|
33149
|
-
tags,
|
|
33150
|
-
page,
|
|
33151
|
-
metric: metric.value,
|
|
33152
|
-
metricJson: metric.json
|
|
33153
|
-
});
|
|
33154
|
-
}
|
|
33155
|
-
return normalized;
|
|
33156
|
-
}
|
|
33157
|
-
function parseStoredJson(value, schema, label) {
|
|
33158
|
-
let decoded;
|
|
33159
|
-
try {
|
|
33160
|
-
decoded = JSON.parse(value);
|
|
33161
|
-
} catch {
|
|
33162
|
-
throw new SeoError("INTERNAL_ERROR", `${label} contains invalid JSON.`);
|
|
33163
|
-
}
|
|
33164
|
-
const parsed = schema.safeParse(decoded);
|
|
33165
|
-
if (!parsed.success) throw new SeoError("INTERNAL_ERROR", `${label} has an invalid shape.`);
|
|
33166
|
-
return parsed.data;
|
|
33167
|
-
}
|
|
33168
|
-
//#endregion
|
|
33169
|
-
//#region packages/core/src/keyword-sets/rows.ts
|
|
33170
|
-
const SET_SELECT = `
|
|
33171
|
-
SELECT sets.*,
|
|
33172
|
-
(SELECT COUNT(*) FROM keyword_set_items items WHERE items.set_id = sets.id) AS keyword_count,
|
|
33173
|
-
(SELECT COUNT(DISTINCT tags.tag) FROM keyword_set_tags tags WHERE tags.set_id = sets.id) AS tag_count
|
|
33174
|
-
FROM keyword_sets sets`;
|
|
33175
|
-
function storeDatabase(options) {
|
|
33176
|
-
return options.database ?? getDb();
|
|
33177
|
-
}
|
|
33178
|
-
function rowToSet(row) {
|
|
33179
|
-
const provider = row.provider ? providerIdSchema.safeParse(row.provider) : void 0;
|
|
33180
|
-
if (provider && !provider.success) throw new SeoError("INTERNAL_ERROR", "Saved keyword set provider is invalid.");
|
|
33181
|
-
return {
|
|
33182
|
-
schemaVersion: 1,
|
|
33183
|
-
id: row.id,
|
|
33184
|
-
projectId: row.project_id,
|
|
33185
|
-
name: row.name,
|
|
33186
|
-
market: parseStoredJson(row.market_json, searchMarketSchema, "Saved search market"),
|
|
33187
|
-
provider: provider?.data ?? null,
|
|
33188
|
-
sourceReport: row.source_report,
|
|
33189
|
-
keywordCount: row.keyword_count,
|
|
33190
|
-
tagCount: row.tag_count,
|
|
33191
|
-
createdAt: new Date(row.created_at).toISOString(),
|
|
33192
|
-
updatedAt: new Date(row.updated_at).toISOString(),
|
|
33193
|
-
lastRefreshedAt: row.last_refreshed_at === null ? null : new Date(row.last_refreshed_at).toISOString()
|
|
33194
|
-
};
|
|
33195
|
-
}
|
|
33196
|
-
function findSetRow(db, projectId, idOrName) {
|
|
33197
|
-
return db.prepare(`${SET_SELECT}
|
|
33198
|
-
WHERE sets.project_id = ?
|
|
33199
|
-
AND (sets.id = ? OR sets.name = ? COLLATE NOCASE)
|
|
33200
|
-
ORDER BY CASE WHEN sets.id = ? THEN 0 ELSE 1 END
|
|
33201
|
-
LIMIT 1`).get(projectId, idOrName, idOrName, idOrName);
|
|
33202
|
-
}
|
|
33203
|
-
function requireSetRow(db, projectId, idOrName) {
|
|
33204
|
-
const row = findSetRow(db, projectId, idOrName);
|
|
33205
|
-
if (!row) invalid(`Keyword set not found: ${idOrName}`);
|
|
33206
|
-
return row;
|
|
33207
|
-
}
|
|
33208
|
-
function rowCount(db, sql, ...params) {
|
|
33209
|
-
return db.prepare(sql).get(...params).count;
|
|
33210
|
-
}
|
|
33211
|
-
function keywordSetLogicalBytes(db = getDb()) {
|
|
33212
|
-
return db.prepare(`SELECT
|
|
33213
|
-
COALESCE((SELECT SUM(length(id) + length(project_id) + length(name) + length(market_json) + COALESCE(length(provider), 0) + COALESCE(length(source_report), 0)) FROM keyword_sets), 0) +
|
|
33214
|
-
COALESCE((SELECT SUM(length(set_id) + length(normalized_keyword) + length(display_keyword) + COALESCE(length(metric_json), 0) + COALESCE(length(metric_provider), 0) + COALESCE(length(metric_observed_at), 0) + COALESCE(length(page_kind), 0) + COALESCE(length(page_url), 0)) FROM keyword_set_items), 0) +
|
|
33215
|
-
COALESCE((SELECT SUM(length(set_id) + length(normalized_keyword) + length(tag)) FROM keyword_set_tags), 0)
|
|
33216
|
-
AS bytes`).get().bytes;
|
|
33217
|
-
}
|
|
33218
|
-
function enforceKeywordSetLogicalBytes(db, maxLogicalBytes = KEYWORD_SET_LIMITS.logicalBytes) {
|
|
33219
|
-
if (keywordSetLogicalBytes(db) > maxLogicalBytes) invalid(`Saved keyword data cannot exceed ${maxLogicalBytes} logical bytes. Remove an unused set before adding more.`);
|
|
33220
|
-
}
|
|
33221
|
-
//#endregion
|
|
33222
34654
|
//#region packages/core/src/keyword-sets/mutations.ts
|
|
33223
34655
|
function addKeywordsToSet(input, options = {}) {
|
|
33224
34656
|
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
@@ -33232,8 +34664,8 @@ function addKeywordsToSet(input, options = {}) {
|
|
|
33232
34664
|
WHERE set_id = ? AND normalized_keyword IN (${[...items].map(() => "?").join(", ")})`).all(set.id, ...items.keys());
|
|
33233
34665
|
const existing = new Map(existingRows.map((row) => [row.normalized_keyword, row]));
|
|
33234
34666
|
const additions = items.size - existing.size;
|
|
33235
|
-
if (currentSetCount + additions > KEYWORD_SET_LIMITS.keywordsPerSet) invalid(`A keyword set can contain at most ${KEYWORD_SET_LIMITS.keywordsPerSet} keywords.`);
|
|
33236
|
-
if (currentTotalCount + additions > KEYWORD_SET_LIMITS.totalKeywords) invalid(`At most ${KEYWORD_SET_LIMITS.totalKeywords} keywords can be saved across all sets.`);
|
|
34667
|
+
if (currentSetCount + additions > KEYWORD_SET_LIMITS.keywordsPerSet) invalid$1(`A keyword set can contain at most ${KEYWORD_SET_LIMITS.keywordsPerSet} keywords.`);
|
|
34668
|
+
if (currentTotalCount + additions > KEYWORD_SET_LIMITS.totalKeywords) invalid$1(`At most ${KEYWORD_SET_LIMITS.totalKeywords} keywords can be saved across all sets.`);
|
|
33237
34669
|
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33238
34670
|
const updated = /* @__PURE__ */ new Set();
|
|
33239
34671
|
db.transaction(() => {
|
|
@@ -33291,7 +34723,7 @@ function addTags(db, setId, normalizedKeyword, tags) {
|
|
|
33291
34723
|
return changed;
|
|
33292
34724
|
}
|
|
33293
34725
|
function removeKeywordsFromSet(input, options = {}) {
|
|
33294
|
-
if (input.keywords.length < 1 || input.keywords.length > KEYWORD_SET_LIMITS.mutationKeywords) invalid(`Remove 1 to ${KEYWORD_SET_LIMITS.mutationKeywords} keywords per operation.`);
|
|
34726
|
+
if (input.keywords.length < 1 || input.keywords.length > KEYWORD_SET_LIMITS.mutationKeywords) invalid$1(`Remove 1 to ${KEYWORD_SET_LIMITS.mutationKeywords} keywords per operation.`);
|
|
33295
34727
|
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33296
34728
|
const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33297
34729
|
const keywords = [...new Set(input.keywords.map(normalizeSavedKeyword))];
|
|
@@ -33317,7 +34749,7 @@ function setKeywordSetRefreshTime(input, options = {}) {
|
|
|
33317
34749
|
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33318
34750
|
const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33319
34751
|
const refreshedAt = Date.parse(input.refreshedAt);
|
|
33320
|
-
if (!Number.isFinite(refreshedAt)) invalid("Use a valid refresh timestamp.");
|
|
34752
|
+
if (!Number.isFinite(refreshedAt)) invalid$1("Use a valid refresh timestamp.");
|
|
33321
34753
|
const db = storeDatabase(options);
|
|
33322
34754
|
const row = requireSetRow(db, projectId, idOrName);
|
|
33323
34755
|
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
@@ -33325,122 +34757,6 @@ function setKeywordSetRefreshTime(input, options = {}) {
|
|
|
33325
34757
|
return rowToSet(requireSetRow(db, projectId, row.id));
|
|
33326
34758
|
}
|
|
33327
34759
|
//#endregion
|
|
33328
|
-
//#region packages/core/src/keyword-sets/store.ts
|
|
33329
|
-
function createKeywordSet(input, options = {}) {
|
|
33330
|
-
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33331
|
-
const name = boundedText(input.name, "Keyword set name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33332
|
-
const market = searchMarketSchema.safeParse(input.market);
|
|
33333
|
-
if (!market.success) invalid("Use a valid search market.");
|
|
33334
|
-
const provider = input.provider === void 0 ? void 0 : providerIdSchema.safeParse(input.provider);
|
|
33335
|
-
if (provider && !provider.success) invalid("Use a supported keyword provider.");
|
|
33336
|
-
const sourceReport = input.sourceReport ? boundedText(input.sourceReport, "Source report", KEYWORD_SET_FIELD_LIMITS.sourceReport) : null;
|
|
33337
|
-
const db = storeDatabase(options);
|
|
33338
|
-
if (rowCount(db, "SELECT COUNT(*) AS count FROM keyword_sets") >= KEYWORD_SET_LIMITS.totalSets) invalid(`At most ${KEYWORD_SET_LIMITS.totalSets} keyword sets can be saved.`);
|
|
33339
|
-
if (rowCount(db, "SELECT COUNT(*) AS count FROM keyword_sets WHERE project_id = ?", projectId) >= KEYWORD_SET_LIMITS.setsPerProject) invalid(`A project can have at most ${KEYWORD_SET_LIMITS.setsPerProject} keyword sets.`);
|
|
33340
|
-
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
33341
|
-
const id = (options.id ?? randomUUID)();
|
|
33342
|
-
try {
|
|
33343
|
-
db.transaction(() => {
|
|
33344
|
-
db.prepare(`INSERT INTO keyword_sets
|
|
33345
|
-
(id, project_id, name, market_json, provider, source_report, created_at, updated_at)
|
|
33346
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, projectId, name, JSON.stringify(market.data), provider?.data ?? null, sourceReport, now, now);
|
|
33347
|
-
enforceKeywordSetLogicalBytes(db, options.maxLogicalBytes);
|
|
33348
|
-
}).immediate();
|
|
33349
|
-
} catch (error) {
|
|
33350
|
-
if (/unique constraint/i.test(String(error))) invalid(`A keyword set named ${name} already exists for this project.`);
|
|
33351
|
-
throw error;
|
|
33352
|
-
}
|
|
33353
|
-
return rowToSet(requireSetRow(db, projectId, id));
|
|
33354
|
-
}
|
|
33355
|
-
function listKeywordSets(input, options = {}) {
|
|
33356
|
-
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33357
|
-
const limit = input.limit ?? 100;
|
|
33358
|
-
const offset = input.offset ?? 0;
|
|
33359
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) invalid("Keyword set list limit must be from 1 to 100.");
|
|
33360
|
-
if (!Number.isSafeInteger(offset) || offset < 0) invalid("Keyword set list offset must be zero or greater.");
|
|
33361
|
-
return storeDatabase(options).prepare(`${SET_SELECT}
|
|
33362
|
-
WHERE sets.project_id = ?
|
|
33363
|
-
ORDER BY sets.updated_at DESC, sets.id
|
|
33364
|
-
LIMIT ? OFFSET ?`).all(projectId, limit, offset).map(rowToSet);
|
|
33365
|
-
}
|
|
33366
|
-
function getKeywordSet(input, options = {}) {
|
|
33367
|
-
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33368
|
-
const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33369
|
-
const tag = input.tag ? boundedText(input.tag, "Tag", KEYWORD_SET_FIELD_LIMITS.tag).toLowerCase() : null;
|
|
33370
|
-
const limit = input.limit ?? 100;
|
|
33371
|
-
const offset = input.offset ?? 0;
|
|
33372
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > KEYWORD_SET_LIMITS.outputRows) invalid(`Keyword set output limit must be from 1 to ${KEYWORD_SET_LIMITS.outputRows}.`);
|
|
33373
|
-
if (!Number.isSafeInteger(offset) || offset < 0) invalid("Keyword set output offset must be zero or greater.");
|
|
33374
|
-
const db = storeDatabase(options);
|
|
33375
|
-
const setRow = requireSetRow(db, projectId, idOrName);
|
|
33376
|
-
const filterSql = tag ? `AND EXISTS (
|
|
33377
|
-
SELECT 1 FROM keyword_set_tags filter_tags
|
|
33378
|
-
WHERE filter_tags.set_id = items.set_id
|
|
33379
|
-
AND filter_tags.normalized_keyword = items.normalized_keyword
|
|
33380
|
-
AND filter_tags.tag = ?
|
|
33381
|
-
)` : "";
|
|
33382
|
-
const params = tag ? [setRow.id, tag] : [setRow.id];
|
|
33383
|
-
const total = rowCount(db, `SELECT COUNT(*) AS count FROM keyword_set_items items
|
|
33384
|
-
WHERE items.set_id = ? ${filterSql}`, ...params);
|
|
33385
|
-
const rows = db.prepare(`SELECT items.* FROM keyword_set_items items
|
|
33386
|
-
WHERE items.set_id = ? ${filterSql}
|
|
33387
|
-
ORDER BY items.normalized_keyword
|
|
33388
|
-
LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
33389
|
-
const tagsByKeyword = loadTags(db, setRow.id, rows);
|
|
33390
|
-
const items = rows.map((row) => itemFromRow(row, tagsByKeyword.get(row.normalized_keyword) ?? []));
|
|
33391
|
-
return {
|
|
33392
|
-
schemaVersion: 1,
|
|
33393
|
-
set: rowToSet(setRow),
|
|
33394
|
-
items,
|
|
33395
|
-
pagination: {
|
|
33396
|
-
offset,
|
|
33397
|
-
limit,
|
|
33398
|
-
returned: items.length,
|
|
33399
|
-
total,
|
|
33400
|
-
nextOffset: offset + items.length < total ? offset + items.length : null
|
|
33401
|
-
},
|
|
33402
|
-
filter: { tag }
|
|
33403
|
-
};
|
|
33404
|
-
}
|
|
33405
|
-
function loadTags(db, setId, rows) {
|
|
33406
|
-
if (rows.length === 0) return /* @__PURE__ */ new Map();
|
|
33407
|
-
const tagRows = db.prepare(`SELECT normalized_keyword, tag FROM keyword_set_tags
|
|
33408
|
-
WHERE set_id = ? AND normalized_keyword IN (${rows.map(() => "?").join(", ")})
|
|
33409
|
-
ORDER BY normalized_keyword, tag`).all(setId, ...rows.map((row) => row.normalized_keyword));
|
|
33410
|
-
const tagsByKeyword = /* @__PURE__ */ new Map();
|
|
33411
|
-
for (const row of tagRows) {
|
|
33412
|
-
const tags = tagsByKeyword.get(row.normalized_keyword) ?? [];
|
|
33413
|
-
tags.push(row.tag);
|
|
33414
|
-
tagsByKeyword.set(row.normalized_keyword, tags);
|
|
33415
|
-
}
|
|
33416
|
-
return tagsByKeyword;
|
|
33417
|
-
}
|
|
33418
|
-
function itemFromRow(row, tags) {
|
|
33419
|
-
const latestMetric = row.metric_json ? parseStoredJson(row.metric_json, savedMetricSchema, "Saved keyword metric") : null;
|
|
33420
|
-
if (latestMetric && (latestMetric.provider !== row.metric_provider || latestMetric.observedAt !== row.metric_observed_at || normalizeSavedKeyword(latestMetric.metric.keyword) !== row.normalized_keyword)) throw new SeoError("INTERNAL_ERROR", "Saved keyword metric metadata is inconsistent.");
|
|
33421
|
-
const page = row.page_kind && row.page_url ? {
|
|
33422
|
-
kind: row.page_kind,
|
|
33423
|
-
url: row.page_url
|
|
33424
|
-
} : null;
|
|
33425
|
-
return {
|
|
33426
|
-
keyword: row.display_keyword,
|
|
33427
|
-
normalizedKeyword: row.normalized_keyword,
|
|
33428
|
-
tags,
|
|
33429
|
-
page,
|
|
33430
|
-
latestMetric,
|
|
33431
|
-
createdAt: new Date(row.created_at).toISOString(),
|
|
33432
|
-
updatedAt: new Date(row.updated_at).toISOString()
|
|
33433
|
-
};
|
|
33434
|
-
}
|
|
33435
|
-
function deleteKeywordSet(input, options = {}) {
|
|
33436
|
-
const projectId = boundedText(input.projectId, "Project id", KEYWORD_SET_FIELD_LIMITS.projectId);
|
|
33437
|
-
const idOrName = boundedText(input.idOrName, "Keyword set id or name", KEYWORD_SET_FIELD_LIMITS.name);
|
|
33438
|
-
const db = storeDatabase(options);
|
|
33439
|
-
const row = findSetRow(db, projectId, idOrName);
|
|
33440
|
-
if (!row) return false;
|
|
33441
|
-
return db.prepare("DELETE FROM keyword_sets WHERE id = ?").run(row.id).changes > 0;
|
|
33442
|
-
}
|
|
33443
|
-
//#endregion
|
|
33444
34760
|
//#region packages/core/src/keyword-sets/refresh.ts
|
|
33445
34761
|
const REFRESH_BATCH_SIZE = 50;
|
|
33446
34762
|
function estimator(adapter) {
|
|
@@ -38391,6 +39707,7 @@ const TELEMETRY_REPORTS = [
|
|
|
38391
39707
|
"pseo-opportunities",
|
|
38392
39708
|
"query-clusters",
|
|
38393
39709
|
"quick-wins",
|
|
39710
|
+
"rank-tracking",
|
|
38394
39711
|
"redirect-trace",
|
|
38395
39712
|
"narrative-report",
|
|
38396
39713
|
"second-page",
|
|
@@ -38719,6 +40036,6 @@ function isoDate(daysAgo = 0) {
|
|
|
38719
40036
|
return date.toISOString().slice(0, 10);
|
|
38720
40037
|
}
|
|
38721
40038
|
//#endregion
|
|
38722
|
-
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, submitIndexNow, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
|
|
40039
|
+
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
|
|
38723
40040
|
|
|
38724
40041
|
//# sourceMappingURL=index.js.map
|