@yoryoboy/bi-mcp 1.0.1
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/README.md +346 -0
- package/bin/bi-mcp.js +2 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.js +768 -0
- package/dist/index.js.map +7 -0
- package/dist/mcp-use.json +7 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/icon.svg +6 -0
- package/dist/src/analytics/ga4-channel-groups.js +20 -0
- package/dist/src/analytics/ga4-channel-groups.js.map +7 -0
- package/dist/src/analytics/ga4-report-utils.js +117 -0
- package/dist/src/analytics/ga4-report-utils.js.map +7 -0
- package/dist/src/config/benchmarks.js +128 -0
- package/dist/src/config/benchmarks.js.map +7 -0
- package/dist/src/config/google.js +41 -0
- package/dist/src/config/google.js.map +7 -0
- package/dist/src/config/vtex.js +26 -0
- package/dist/src/config/vtex.js.map +7 -0
- package/dist/src/google-ads/report-utils.js +78 -0
- package/dist/src/google-ads/report-utils.js.map +7 -0
- package/dist/src/prompts/reporte-ventas.js +75 -0
- package/dist/src/prompts/reporte-ventas.js.map +7 -0
- package/dist/src/search-console/search-console-utils.js +275 -0
- package/dist/src/search-console/search-console-utils.js.map +7 -0
- package/dist/src/services/analytics/ga4-client.js +69 -0
- package/dist/src/services/analytics/ga4-client.js.map +7 -0
- package/dist/src/services/analytics/oauth.js +30 -0
- package/dist/src/services/analytics/oauth.js.map +7 -0
- package/dist/src/services/google-ads/google-ads-client.js +54 -0
- package/dist/src/services/google-ads/google-ads-client.js.map +7 -0
- package/dist/src/services/search-console/search-console-client.js +45 -0
- package/dist/src/services/search-console/search-console-client.js.map +7 -0
- package/dist/src/services/vtex/vtex-api.js +51 -0
- package/dist/src/services/vtex/vtex-api.js.map +7 -0
- package/dist/src/services/vtex/vtex-catalog.js +18 -0
- package/dist/src/services/vtex/vtex-catalog.js.map +7 -0
- package/dist/src/services/vtex/vtex-logistics.js +151 -0
- package/dist/src/services/vtex/vtex-logistics.js.map +7 -0
- package/dist/src/services/vtex/vtex-orders.js +143 -0
- package/dist/src/services/vtex/vtex-orders.js.map +7 -0
- package/dist/src/services/vtex/vtex-pricing.js +17 -0
- package/dist/src/services/vtex/vtex-pricing.js.map +7 -0
- package/dist/src/tools/analytics/attribution-gaps.js +109 -0
- package/dist/src/tools/analytics/attribution-gaps.js.map +7 -0
- package/dist/src/tools/analytics/channel-mix.js +74 -0
- package/dist/src/tools/analytics/channel-mix.js.map +7 -0
- package/dist/src/tools/analytics/ecommerce-tracking-health.js +89 -0
- package/dist/src/tools/analytics/ecommerce-tracking-health.js.map +7 -0
- package/dist/src/tools/analytics/engagement-overview.js +71 -0
- package/dist/src/tools/analytics/engagement-overview.js.map +7 -0
- package/dist/src/tools/analytics/index.js +12 -0
- package/dist/src/tools/analytics/index.js.map +7 -0
- package/dist/src/tools/analytics/list-accessible-properties.js +46 -0
- package/dist/src/tools/analytics/list-accessible-properties.js.map +7 -0
- package/dist/src/tools/analytics/property-info.js +54 -0
- package/dist/src/tools/analytics/property-info.js.map +7 -0
- package/dist/src/tools/analytics/revenue-by-channel.js +70 -0
- package/dist/src/tools/analytics/revenue-by-channel.js.map +7 -0
- package/dist/src/tools/analytics/revenue-overview.js +77 -0
- package/dist/src/tools/analytics/revenue-overview.js.map +7 -0
- package/dist/src/tools/analytics/revenue-trend.js +69 -0
- package/dist/src/tools/analytics/revenue-trend.js.map +7 -0
- package/dist/src/tools/analytics/source-medium-breakdown.js +86 -0
- package/dist/src/tools/analytics/source-medium-breakdown.js.map +7 -0
- package/dist/src/tools/analytics/top-landing-pages.js +79 -0
- package/dist/src/tools/analytics/top-landing-pages.js.map +7 -0
- package/dist/src/tools/google-ads/account-overview.js +103 -0
- package/dist/src/tools/google-ads/account-overview.js.map +7 -0
- package/dist/src/tools/google-ads/account-risks.js +267 -0
- package/dist/src/tools/google-ads/account-risks.js.map +7 -0
- package/dist/src/tools/google-ads/break-even-analysis.js +107 -0
- package/dist/src/tools/google-ads/break-even-analysis.js.map +7 -0
- package/dist/src/tools/google-ads/campaign-performance.js +157 -0
- package/dist/src/tools/google-ads/campaign-performance.js.map +7 -0
- package/dist/src/tools/google-ads/channel-mix.js +129 -0
- package/dist/src/tools/google-ads/channel-mix.js.map +7 -0
- package/dist/src/tools/google-ads/compare-accounts.js +122 -0
- package/dist/src/tools/google-ads/compare-accounts.js.map +7 -0
- package/dist/src/tools/google-ads/customer-clients.js +77 -0
- package/dist/src/tools/google-ads/customer-clients.js.map +7 -0
- package/dist/src/tools/google-ads/customer-info.js +64 -0
- package/dist/src/tools/google-ads/customer-info.js.map +7 -0
- package/dist/src/tools/google-ads/index.js +12 -0
- package/dist/src/tools/google-ads/index.js.map +7 -0
- package/dist/src/tools/google-ads/scaling-health.js +174 -0
- package/dist/src/tools/google-ads/scaling-health.js.map +7 -0
- package/dist/src/tools/google-ads/search-terms-summary.js +131 -0
- package/dist/src/tools/google-ads/search-terms-summary.js.map +7 -0
- package/dist/src/tools/google-ads/time-series.js +126 -0
- package/dist/src/tools/google-ads/time-series.js.map +7 -0
- package/dist/src/tools/index.js +5 -0
- package/dist/src/tools/index.js.map +7 -0
- package/dist/src/tools/search-console/country-breakdown.js +85 -0
- package/dist/src/tools/search-console/country-breakdown.js.map +7 -0
- package/dist/src/tools/search-console/device-breakdown.js +85 -0
- package/dist/src/tools/search-console/device-breakdown.js.map +7 -0
- package/dist/src/tools/search-console/high-impression-low-click-queries.js +95 -0
- package/dist/src/tools/search-console/high-impression-low-click-queries.js.map +7 -0
- package/dist/src/tools/search-console/index.js +15 -0
- package/dist/src/tools/search-console/index.js.map +7 -0
- package/dist/src/tools/search-console/list-accessible-sites.js +42 -0
- package/dist/src/tools/search-console/list-accessible-sites.js.map +7 -0
- package/dist/src/tools/search-console/low-ctr-opportunities.js +98 -0
- package/dist/src/tools/search-console/low-ctr-opportunities.js.map +7 -0
- package/dist/src/tools/search-console/page-performance.js +104 -0
- package/dist/src/tools/search-console/page-performance.js.map +7 -0
- package/dist/src/tools/search-console/product-demand-low-capture-queries.js +93 -0
- package/dist/src/tools/search-console/product-demand-low-capture-queries.js.map +7 -0
- package/dist/src/tools/search-console/query-page-matrix.js +99 -0
- package/dist/src/tools/search-console/query-page-matrix.js.map +7 -0
- package/dist/src/tools/search-console/query-performance.js +109 -0
- package/dist/src/tools/search-console/query-performance.js.map +7 -0
- package/dist/src/tools/search-console/quick-win-opportunities.js +93 -0
- package/dist/src/tools/search-console/quick-win-opportunities.js.map +7 -0
- package/dist/src/tools/search-console/rising-non-brand-queries.js +121 -0
- package/dist/src/tools/search-console/rising-non-brand-queries.js.map +7 -0
- package/dist/src/tools/search-console/search-performance.js +89 -0
- package/dist/src/tools/search-console/search-performance.js.map +7 -0
- package/dist/src/tools/search-console/site-context.js +43 -0
- package/dist/src/tools/search-console/site-context.js.map +7 -0
- package/dist/src/tools/search-console/visibility-declines.js +146 -0
- package/dist/src/tools/search-console/visibility-declines.js.map +7 -0
- package/dist/src/tools/vtex/computed-price.js +48 -0
- package/dist/src/tools/vtex/computed-price.js.map +7 -0
- package/dist/src/tools/vtex/index.js +11 -0
- package/dist/src/tools/vtex/index.js.map +7 -0
- package/dist/src/tools/vtex/inventory-check.js +148 -0
- package/dist/src/tools/vtex/inventory-check.js.map +7 -0
- package/dist/src/tools/vtex/order-details.js +56 -0
- package/dist/src/tools/vtex/order-details.js.map +7 -0
- package/dist/src/tools/vtex/orders-summary.js +83 -0
- package/dist/src/tools/vtex/orders-summary.js.map +7 -0
- package/dist/src/tools/vtex/product-offers.js +28 -0
- package/dist/src/tools/vtex/product-offers.js.map +7 -0
- package/dist/src/tools/vtex/sku-offers.js +30 -0
- package/dist/src/tools/vtex/sku-offers.js.map +7 -0
- package/dist/src/tools/vtex/sku-price.js +42 -0
- package/dist/src/tools/vtex/sku-price.js.map +7 -0
- package/dist/src/tools/vtex/update-inventory.js +43 -0
- package/dist/src/tools/vtex/update-inventory.js.map +7 -0
- package/dist/src/tools/vtex/update-lead-time.js +32 -0
- package/dist/src/tools/vtex/update-lead-time.js.map +7 -0
- package/dist/src/tools/vtex/warehouse-inventory.js +42 -0
- package/dist/src/tools/vtex/warehouse-inventory.js.map +7 -0
- package/dist/src/utils/case-conversion.js +21 -0
- package/dist/src/utils/case-conversion.js.map +7 -0
- package/dist/src/utils/currency.js +52 -0
- package/dist/src/utils/currency.js.map +7 -0
- package/dist/src/utils/format-order-details.js +137 -0
- package/dist/src/utils/format-order-details.js.map +7 -0
- package/dist/src/utils/google-ads.js +78 -0
- package/dist/src/utils/google-ads.js.map +7 -0
- package/dist/src/utils/money.js +83 -0
- package/dist/src/utils/money.js.map +7 -0
- package/dist/src/utils/order-status.js +11 -0
- package/dist/src/utils/order-status.js.map +7 -0
- package/dist/src/utils/pagination.js +45 -0
- package/dist/src/utils/pagination.js.map +7 -0
- package/dist/src/utils/strip-payload.js +40 -0
- package/dist/src/utils/strip-payload.js.map +7 -0
- package/dist/src/utils/type-guards.js +7 -0
- package/dist/src/utils/type-guards.js.map +7 -0
- package/package.json +66 -0
- package/public/favicon.ico +0 -0
- package/public/icon.svg +6 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { markdown } from "mcp-use/server";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PROMPT_TEMPLATE_PATHS = [
|
|
8
|
+
resolve(MODULE_DIR, "prompt_reporte_ventas_semanal.md"),
|
|
9
|
+
resolve(process.cwd(), "src/prompts/prompt_reporte_ventas_semanal.md")
|
|
10
|
+
];
|
|
11
|
+
const reporteVentasPromptSchema = z.object({
|
|
12
|
+
tiempo: z.string().describe(
|
|
13
|
+
"Periodo en lenguaje natural (ej: 'esta semana', 'enero 2026', 'del 2026-01-10 al 2026-01-25')."
|
|
14
|
+
)
|
|
15
|
+
});
|
|
16
|
+
function getBuenosAiresTodayIso() {
|
|
17
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
18
|
+
timeZone: "America/Argentina/Buenos_Aires",
|
|
19
|
+
year: "numeric",
|
|
20
|
+
month: "2-digit",
|
|
21
|
+
day: "2-digit"
|
|
22
|
+
}).formatToParts(/* @__PURE__ */ new Date());
|
|
23
|
+
const year = parts.find((part) => part.type === "year")?.value ?? "0000";
|
|
24
|
+
const month = parts.find((part) => part.type === "month")?.value ?? "00";
|
|
25
|
+
const day = parts.find((part) => part.type === "day")?.value ?? "00";
|
|
26
|
+
return `${year}-${month}-${day}`;
|
|
27
|
+
}
|
|
28
|
+
function loadReporteTemplate() {
|
|
29
|
+
for (const path of PROMPT_TEMPLATE_PATHS) {
|
|
30
|
+
if (existsSync(path)) {
|
|
31
|
+
return readFileSync(path, "utf8").trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Prompt template file not found. Checked: ${PROMPT_TEMPLATE_PATHS.join(", ")}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
async function reporteVentasPromptHandler({
|
|
39
|
+
tiempo
|
|
40
|
+
}) {
|
|
41
|
+
const todayIso = getBuenosAiresTodayIso();
|
|
42
|
+
const template = loadReporteTemplate();
|
|
43
|
+
const invocationBlock = `
|
|
44
|
+
## Contexto de invocacion
|
|
45
|
+
|
|
46
|
+
- Solicitud temporal del usuario: "${tiempo}"
|
|
47
|
+
- Fecha de referencia para interpretar relativos: ${todayIso} (America/Argentina/Buenos_Aires)
|
|
48
|
+
|
|
49
|
+
## Reglas de interpretacion temporal (obligatorias)
|
|
50
|
+
|
|
51
|
+
1. Converti la solicitud temporal del usuario a fechas absolutas \`startDate\` y \`endDate\` en formato \`YYYY-MM-DD\`.
|
|
52
|
+
2. Si el usuario pide semana, usa rango lunes-domingo.
|
|
53
|
+
3. Si pide mes, usa el primer y ultimo dia del mes.
|
|
54
|
+
4. Si pide anio, usa del \`YYYY-01-01\` al \`YYYY-12-31\`.
|
|
55
|
+
5. Si pide un rango explicito, respeta exactamente ese rango.
|
|
56
|
+
6. Si usa expresiones relativas (por ejemplo "ultimos 15 dias"), resolvelas usando la fecha de referencia.
|
|
57
|
+
7. Si la solicitud es ambigua y no se puede convertir con seguridad a fechas exactas, pedi una aclaracion minima antes de ejecutar herramientas.
|
|
58
|
+
|
|
59
|
+
## Ejecucion de herramientas
|
|
60
|
+
|
|
61
|
+
- Ejecuta siempre \`vtex_get_orders_summary\` para obtener el universo de ordenes del periodo.
|
|
62
|
+
- Luego ejecuta \`vtex_get_order_details\` cubriendo el 100% de los \`order_id\` (lotes de hasta 50 IDs por llamada).
|
|
63
|
+
- Con esos datos, genera el reporte con la estructura y reglas del template siguiente.
|
|
64
|
+
`.trim();
|
|
65
|
+
return markdown(`${invocationBlock}
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
${template}`);
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
reporteVentasPromptHandler,
|
|
73
|
+
reporteVentasPromptSchema
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=reporte-ventas.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/prompts/reporte-ventas.ts"],
|
|
4
|
+
"sourcesContent": ["import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { markdown } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url));\nconst PROMPT_TEMPLATE_PATHS = [\n resolve(MODULE_DIR, \"prompt_reporte_ventas_semanal.md\"),\n resolve(process.cwd(), \"src/prompts/prompt_reporte_ventas_semanal.md\"),\n];\n\nexport const reporteVentasPromptSchema = z.object({\n tiempo: z\n .string()\n .describe(\n \"Periodo en lenguaje natural (ej: 'esta semana', 'enero 2026', 'del 2026-01-10 al 2026-01-25').\"\n ),\n});\n\nfunction getBuenosAiresTodayIso(): string {\n const parts = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: \"America/Argentina/Buenos_Aires\",\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n }).formatToParts(new Date());\n\n const year = parts.find((part) => part.type === \"year\")?.value ?? \"0000\";\n const month = parts.find((part) => part.type === \"month\")?.value ?? \"00\";\n const day = parts.find((part) => part.type === \"day\")?.value ?? \"00\";\n\n return `${year}-${month}-${day}`;\n}\n\nfunction loadReporteTemplate(): string {\n for (const path of PROMPT_TEMPLATE_PATHS) {\n if (existsSync(path)) {\n return readFileSync(path, \"utf8\").trim();\n }\n }\n\n throw new Error(\n `Prompt template file not found. Checked: ${PROMPT_TEMPLATE_PATHS.join(\", \")}`\n );\n}\n\nexport async function reporteVentasPromptHandler({\n tiempo,\n}: z.infer<typeof reporteVentasPromptSchema>) {\n const todayIso = getBuenosAiresTodayIso();\n const template = loadReporteTemplate();\n\n const invocationBlock = `\n## Contexto de invocacion\n\n- Solicitud temporal del usuario: \"${tiempo}\"\n- Fecha de referencia para interpretar relativos: ${todayIso} (America/Argentina/Buenos_Aires)\n\n## Reglas de interpretacion temporal (obligatorias)\n\n1. Converti la solicitud temporal del usuario a fechas absolutas \\`startDate\\` y \\`endDate\\` en formato \\`YYYY-MM-DD\\`.\n2. Si el usuario pide semana, usa rango lunes-domingo.\n3. Si pide mes, usa el primer y ultimo dia del mes.\n4. Si pide anio, usa del \\`YYYY-01-01\\` al \\`YYYY-12-31\\`.\n5. Si pide un rango explicito, respeta exactamente ese rango.\n6. Si usa expresiones relativas (por ejemplo \"ultimos 15 dias\"), resolvelas usando la fecha de referencia.\n7. Si la solicitud es ambigua y no se puede convertir con seguridad a fechas exactas, pedi una aclaracion minima antes de ejecutar herramientas.\n\n## Ejecucion de herramientas\n\n- Ejecuta siempre \\`vtex_get_orders_summary\\` para obtener el universo de ordenes del periodo.\n- Luego ejecuta \\`vtex_get_order_details\\` cubriendo el 100% de los \\`order_id\\` (lotes de hasta 50 IDs por llamada).\n- Con esos datos, genera el reporte con la estructura y reglas del template siguiente.\n`.trim();\n\n return markdown(`${invocationBlock}\\n\\n---\\n\\n${template}`);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,SAAS,eAAe;AACjC,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,SAAS;AAElB,MAAM,aAAa,QAAQ,cAAc,YAAY,GAAG,CAAC;AACzD,MAAM,wBAAwB;AAAA,EAC5B,QAAQ,YAAY,kCAAkC;AAAA,EACtD,QAAQ,QAAQ,IAAI,GAAG,8CAA8C;AACvE;AAEO,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,QAAQ,EACL,OAAO,EACP;AAAA,IACC;AAAA,EACF;AACJ,CAAC;AAED,SAAS,yBAAiC;AACxC,QAAM,QAAQ,IAAI,KAAK,eAAe,SAAS;AAAA,IAC7C,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,EACP,CAAC,EAAE,cAAc,oBAAI,KAAK,CAAC;AAE3B,QAAM,OAAO,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM,GAAG,SAAS;AAClE,QAAM,QAAQ,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,OAAO,GAAG,SAAS;AACpE,QAAM,MAAM,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,KAAK,GAAG,SAAS;AAEhE,SAAO,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG;AAChC;AAEA,SAAS,sBAA8B;AACrC,aAAW,QAAQ,uBAAuB;AACxC,QAAI,WAAW,IAAI,GAAG;AACpB,aAAO,aAAa,MAAM,MAAM,EAAE,KAAK;AAAA,IACzC;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR,4CAA4C,sBAAsB,KAAK,IAAI,CAAC;AAAA,EAC9E;AACF;AAEA,eAAsB,2BAA2B;AAAA,EAC/C;AACF,GAA8C;AAC5C,QAAM,WAAW,uBAAuB;AACxC,QAAM,WAAW,oBAAoB;AAErC,QAAM,kBAAkB;AAAA;AAAA;AAAA,qCAGW,MAAM;AAAA,oDACS,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiB1D,KAAK;AAEL,SAAO,SAAS,GAAG,eAAe;AAAA;AAAA;AAAA;AAAA,EAAc,QAAQ,EAAE;AAC5D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const searchConsoleDateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
|
+
const searchConsoleSearchTypes = ["web", "image", "video", "news", "discover"];
|
|
4
|
+
const searchConsoleDimensions = [
|
|
5
|
+
"query",
|
|
6
|
+
"page",
|
|
7
|
+
"country",
|
|
8
|
+
"device",
|
|
9
|
+
"date",
|
|
10
|
+
"searchAppearance"
|
|
11
|
+
];
|
|
12
|
+
const searchConsoleFilterOperators = [
|
|
13
|
+
"contains",
|
|
14
|
+
"equals",
|
|
15
|
+
"notContains",
|
|
16
|
+
"notEquals",
|
|
17
|
+
"includingRegex",
|
|
18
|
+
"excludingRegex"
|
|
19
|
+
];
|
|
20
|
+
const searchConsoleAggregationTypes = ["auto", "byPage", "byProperty"];
|
|
21
|
+
const searchConsoleDimensionFilterSchema = z.object({
|
|
22
|
+
dimension: z.enum(searchConsoleDimensions).describe("Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)"),
|
|
23
|
+
operator: z.enum(searchConsoleFilterOperators).describe("Filter operator supported by Search Console."),
|
|
24
|
+
expression: z.string().min(1).describe("Filter value or regex expression.")
|
|
25
|
+
});
|
|
26
|
+
const siteUrlSchemaField = z.string().optional().describe(
|
|
27
|
+
"Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool expects the site to be configured elsewhere."
|
|
28
|
+
);
|
|
29
|
+
const profileIdSchemaField = z.string().optional().describe("Optional business profile identifier for future multi-profile resolution. Defaults to the active profile when supported.");
|
|
30
|
+
const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Start date in YYYY-MM-DD format.");
|
|
31
|
+
const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("End date in YYYY-MM-DD format.");
|
|
32
|
+
const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period start date in YYYY-MM-DD format.");
|
|
33
|
+
const currentEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period end date in YYYY-MM-DD format.");
|
|
34
|
+
const previousStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Previous comparison period start date in YYYY-MM-DD format.");
|
|
35
|
+
const previousEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Previous comparison period end date in YYYY-MM-DD format.");
|
|
36
|
+
const rowLimitSchemaField = z.number().int().min(1).max(25e3).optional().describe("Maximum number of rows to return. Search Console supports up to 25,000 rows per request.");
|
|
37
|
+
const startRowSchemaField = z.number().int().min(0).optional().describe("Zero-based row offset for paginated Search Console requests.");
|
|
38
|
+
const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe("Search type to query. Defaults to web.");
|
|
39
|
+
const brandTermsSchemaField = z.array(z.string().min(1)).max(50).optional().describe("Brand tokens used to separate branded from non-branded queries.");
|
|
40
|
+
function resolveSearchConsoleSiteUrl(siteUrl, profileId) {
|
|
41
|
+
const explicitSiteUrl = siteUrl?.trim();
|
|
42
|
+
if (explicitSiteUrl) {
|
|
43
|
+
return explicitSiteUrl;
|
|
44
|
+
}
|
|
45
|
+
if (profileId?.trim()) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Missing Search Console site URL. profileId "${profileId}" was provided, but business-data based site resolution is not implemented yet. Pass siteUrl explicitly or configure Search Console site resolution first.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Missing Search Console site URL. Pass siteUrl explicitly or first call gsc_list_accessible_sites to discover available properties."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function inferSearchConsolePropertyType(siteUrl) {
|
|
55
|
+
if (siteUrl.startsWith("sc-domain:")) {
|
|
56
|
+
return "domain";
|
|
57
|
+
}
|
|
58
|
+
if (/^https?:\/\//.test(siteUrl)) {
|
|
59
|
+
return "url_prefix";
|
|
60
|
+
}
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
function normalizePermissionLevel(permissionLevel) {
|
|
64
|
+
return permissionLevel?.trim().toLowerCase() || void 0;
|
|
65
|
+
}
|
|
66
|
+
function normalizeSearchConsoleSiteEntry(siteEntry) {
|
|
67
|
+
return {
|
|
68
|
+
site_url: siteEntry.siteUrl,
|
|
69
|
+
property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),
|
|
70
|
+
permission_level: normalizePermissionLevel(siteEntry.permissionLevel)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function toPercent(numerator, denominator) {
|
|
74
|
+
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
return numerator / denominator * 100;
|
|
78
|
+
}
|
|
79
|
+
function round(value, decimals = 2) {
|
|
80
|
+
if (!Number.isFinite(value)) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const factor = 10 ** decimals;
|
|
84
|
+
return Math.round(value * factor) / factor;
|
|
85
|
+
}
|
|
86
|
+
function normalizeSearchConsoleRow(row, dimensions = []) {
|
|
87
|
+
const clicks = row.clicks ?? 0;
|
|
88
|
+
const impressions = row.impressions ?? 0;
|
|
89
|
+
const ctr = typeof row.ctr === "number" ? row.ctr * 100 : toPercent(clicks, impressions);
|
|
90
|
+
const keys = row.keys ?? [];
|
|
91
|
+
return {
|
|
92
|
+
keys,
|
|
93
|
+
dimensions: dimensions.reduce((accumulator, dimension, index) => {
|
|
94
|
+
accumulator[dimension] = keys[index] ?? "";
|
|
95
|
+
return accumulator;
|
|
96
|
+
}, {}),
|
|
97
|
+
clicks: round(clicks, 2),
|
|
98
|
+
impressions: round(impressions, 2),
|
|
99
|
+
ctr_percent: round(ctr, 2),
|
|
100
|
+
position: round(row.position ?? 0, 2)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function buildSearchConsoleQueryBody(input) {
|
|
104
|
+
const dimensionFilterGroups = input.dimensionFilters && input.dimensionFilters.length > 0 ? [
|
|
105
|
+
{
|
|
106
|
+
groupType: "and",
|
|
107
|
+
filters: input.dimensionFilters
|
|
108
|
+
}
|
|
109
|
+
] : void 0;
|
|
110
|
+
return {
|
|
111
|
+
startDate: input.startDate,
|
|
112
|
+
endDate: input.endDate,
|
|
113
|
+
dimensions: input.dimensions,
|
|
114
|
+
type: input.searchType ?? "web",
|
|
115
|
+
dimensionFilterGroups,
|
|
116
|
+
aggregationType: input.aggregationType,
|
|
117
|
+
rowLimit: input.rowLimit,
|
|
118
|
+
startRow: input.startRow
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function buildPaginationMetadata(input) {
|
|
122
|
+
const startRow = input.startRow ?? 0;
|
|
123
|
+
const rowLimit = input.rowLimit;
|
|
124
|
+
const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;
|
|
125
|
+
const nextStartRow = isPartial ? startRow + input.returnedRows : void 0;
|
|
126
|
+
return {
|
|
127
|
+
is_partial: isPartial,
|
|
128
|
+
start_row: startRow,
|
|
129
|
+
row_limit: rowLimit,
|
|
130
|
+
returned_rows: input.returnedRows,
|
|
131
|
+
next_start_row: nextStartRow,
|
|
132
|
+
continuation_instructions: nextStartRow ? "This Search Console response may be truncated at row_limit. If you need additional rows, call the same tool again with startRow set to next_start_row." : void 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parseBrandTerms(brandTerms) {
|
|
136
|
+
return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
function isBrandedQuery(query, brandTerms) {
|
|
139
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
140
|
+
const normalizedBrandTerms = parseBrandTerms(brandTerms);
|
|
141
|
+
if (!normalizedQuery || normalizedBrandTerms.length === 0) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));
|
|
145
|
+
}
|
|
146
|
+
function classifyBrandLabel(query, brandTerms) {
|
|
147
|
+
return isBrandedQuery(query, brandTerms) ? "brand" : "non_brand";
|
|
148
|
+
}
|
|
149
|
+
function computeChangePercent(currentValue, previousValue) {
|
|
150
|
+
if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (previousValue === 0) {
|
|
154
|
+
if (currentValue === 0) {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return round((currentValue - previousValue) / previousValue * 100, 2);
|
|
160
|
+
}
|
|
161
|
+
function sumSearchConsoleRows(rows) {
|
|
162
|
+
const totals = rows.reduce(
|
|
163
|
+
(accumulator, row) => {
|
|
164
|
+
accumulator.clicks += row.clicks;
|
|
165
|
+
accumulator.impressions += row.impressions;
|
|
166
|
+
accumulator.weightedPosition += row.position * row.impressions;
|
|
167
|
+
return accumulator;
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
clicks: 0,
|
|
171
|
+
impressions: 0,
|
|
172
|
+
weightedPosition: 0
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
clicks: round(totals.clicks, 2),
|
|
177
|
+
impressions: round(totals.impressions, 2),
|
|
178
|
+
ctrPercent: round(toPercent(totals.clicks, totals.impressions), 2),
|
|
179
|
+
averagePosition: totals.impressions > 0 ? round(totals.weightedPosition / totals.impressions, 2) : 0
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function buildComparisonMap(rows, keyBuilder) {
|
|
183
|
+
return new Map(rows.map((row) => [keyBuilder(row), row]));
|
|
184
|
+
}
|
|
185
|
+
function tokenizeQuery(query) {
|
|
186
|
+
return query.toLowerCase().split(/[^a-z0-9áéíóúüñ]+/i).map((token) => token.trim()).filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
function looksLikeCommercialQuery(query) {
|
|
189
|
+
const tokens = tokenizeQuery(query);
|
|
190
|
+
const commercialTerms = /* @__PURE__ */ new Set([
|
|
191
|
+
"comprar",
|
|
192
|
+
"precio",
|
|
193
|
+
"precios",
|
|
194
|
+
"cuanto",
|
|
195
|
+
"oferta",
|
|
196
|
+
"ofertas",
|
|
197
|
+
"envio",
|
|
198
|
+
"shop",
|
|
199
|
+
"tienda",
|
|
200
|
+
"modelo",
|
|
201
|
+
"medida",
|
|
202
|
+
"ml",
|
|
203
|
+
"cm",
|
|
204
|
+
"kg"
|
|
205
|
+
]);
|
|
206
|
+
return tokens.some((token) => commercialTerms.has(token));
|
|
207
|
+
}
|
|
208
|
+
function looksLikeProductQuery(query, productTerms) {
|
|
209
|
+
const normalizedProductTerms = parseBrandTerms(productTerms);
|
|
210
|
+
const normalizedQuery = query.toLowerCase();
|
|
211
|
+
if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;
|
|
215
|
+
}
|
|
216
|
+
function classifyQuickWinType(position, ctrPercent) {
|
|
217
|
+
if (position <= 8 && ctrPercent <= 3) {
|
|
218
|
+
return "ctr";
|
|
219
|
+
}
|
|
220
|
+
return "position";
|
|
221
|
+
}
|
|
222
|
+
function scoreHighImpressionLowClickOpportunity(input) {
|
|
223
|
+
const impressionScore = Math.min(input.impressions / 100, 60);
|
|
224
|
+
const ctrPenalty = Math.max(0, 15 - input.ctrPercent) * 2;
|
|
225
|
+
const positionBonus = input.position > 0 && input.position <= 20 ? Math.max(0, 20 - input.position) : 0;
|
|
226
|
+
return round(impressionScore + ctrPenalty + positionBonus, 2);
|
|
227
|
+
}
|
|
228
|
+
function scoreQuickWinOpportunity(input) {
|
|
229
|
+
const impressionWeight = Math.min(input.impressions / 150, 50);
|
|
230
|
+
const ctrWeight = Math.max(0, 10 - input.ctrPercent) * 3;
|
|
231
|
+
const positionWeight = input.position > 0 && input.position <= 20 ? Math.max(0, 18 - input.position) * 1.8 : 0;
|
|
232
|
+
return round(impressionWeight + ctrWeight + positionWeight, 2);
|
|
233
|
+
}
|
|
234
|
+
export {
|
|
235
|
+
brandTermsSchemaField,
|
|
236
|
+
buildComparisonMap,
|
|
237
|
+
buildPaginationMetadata,
|
|
238
|
+
buildSearchConsoleQueryBody,
|
|
239
|
+
classifyBrandLabel,
|
|
240
|
+
classifyQuickWinType,
|
|
241
|
+
computeChangePercent,
|
|
242
|
+
currentEndDateSchemaField,
|
|
243
|
+
currentStartDateSchemaField,
|
|
244
|
+
endDateSchemaField,
|
|
245
|
+
inferSearchConsolePropertyType,
|
|
246
|
+
isBrandedQuery,
|
|
247
|
+
looksLikeCommercialQuery,
|
|
248
|
+
looksLikeProductQuery,
|
|
249
|
+
normalizePermissionLevel,
|
|
250
|
+
normalizeSearchConsoleRow,
|
|
251
|
+
normalizeSearchConsoleSiteEntry,
|
|
252
|
+
parseBrandTerms,
|
|
253
|
+
previousEndDateSchemaField,
|
|
254
|
+
previousStartDateSchemaField,
|
|
255
|
+
profileIdSchemaField,
|
|
256
|
+
resolveSearchConsoleSiteUrl,
|
|
257
|
+
round,
|
|
258
|
+
rowLimitSchemaField,
|
|
259
|
+
scoreHighImpressionLowClickOpportunity,
|
|
260
|
+
scoreQuickWinOpportunity,
|
|
261
|
+
searchConsoleAggregationTypes,
|
|
262
|
+
searchConsoleDateRegex,
|
|
263
|
+
searchConsoleDimensionFilterSchema,
|
|
264
|
+
searchConsoleDimensions,
|
|
265
|
+
searchConsoleFilterOperators,
|
|
266
|
+
searchConsoleSearchTypes,
|
|
267
|
+
searchTypeSchemaField,
|
|
268
|
+
siteUrlSchemaField,
|
|
269
|
+
startDateSchemaField,
|
|
270
|
+
startRowSchemaField,
|
|
271
|
+
sumSearchConsoleRows,
|
|
272
|
+
toPercent,
|
|
273
|
+
tokenizeQuery
|
|
274
|
+
};
|
|
275
|
+
//# sourceMappingURL=search-console-utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/search-console/search-console-utils.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from \"zod\";\n\nimport type {\n SearchConsoleDimensionFilter,\n SearchConsoleDimensionFilterGroup,\n SearchConsoleSearchAnalyticsRequest,\n SearchConsoleSearchAnalyticsRow,\n SearchConsoleSiteEntry,\n} from \"../services/search-console/search-console-client.js\";\n\nexport const searchConsoleDateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const searchConsoleSearchTypes = [\"web\", \"image\", \"video\", \"news\", \"discover\"] as const;\nexport const searchConsoleDimensions = [\n \"query\",\n \"page\",\n \"country\",\n \"device\",\n \"date\",\n \"searchAppearance\",\n] as const;\nexport const searchConsoleFilterOperators = [\n \"contains\",\n \"equals\",\n \"notContains\",\n \"notEquals\",\n \"includingRegex\",\n \"excludingRegex\",\n] as const;\nexport const searchConsoleAggregationTypes = [\"auto\", \"byPage\", \"byProperty\"] as const;\n\nexport const searchConsoleDimensionFilterSchema = z.object({\n dimension: z\n .enum(searchConsoleDimensions)\n .describe(\"Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)\"),\n operator: z\n .enum(searchConsoleFilterOperators)\n .describe(\"Filter operator supported by Search Console.\"),\n expression: z.string().min(1).describe(\"Filter value or regex expression.\"),\n});\n\nexport const siteUrlSchemaField = z\n .string()\n .optional()\n .describe(\n \"Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool expects the site to be configured elsewhere.\"\n );\n\nexport const profileIdSchemaField = z\n .string()\n .optional()\n .describe(\"Optional business profile identifier for future multi-profile resolution. Defaults to the active profile when supported.\");\n\nexport const startDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Start date in YYYY-MM-DD format.\");\n\nexport const endDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"End date in YYYY-MM-DD format.\");\n\nexport const currentStartDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Current comparison period start date in YYYY-MM-DD format.\");\n\nexport const currentEndDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Current comparison period end date in YYYY-MM-DD format.\");\n\nexport const previousStartDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Previous comparison period start date in YYYY-MM-DD format.\");\n\nexport const previousEndDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Previous comparison period end date in YYYY-MM-DD format.\");\n\nexport const rowLimitSchemaField = z\n .number()\n .int()\n .min(1)\n .max(25000)\n .optional()\n .describe(\"Maximum number of rows to return. Search Console supports up to 25,000 rows per request.\");\n\nexport const startRowSchemaField = z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\"Zero-based row offset for paginated Search Console requests.\");\n\nexport const searchTypeSchemaField = z\n .enum(searchConsoleSearchTypes)\n .optional()\n .describe(\"Search type to query. Defaults to web.\");\n\nexport const brandTermsSchemaField = z\n .array(z.string().min(1))\n .max(50)\n .optional()\n .describe(\"Brand tokens used to separate branded from non-branded queries.\");\n\nexport function resolveSearchConsoleSiteUrl(siteUrl?: string, profileId?: string): string {\n const explicitSiteUrl = siteUrl?.trim();\n if (explicitSiteUrl) {\n return explicitSiteUrl;\n }\n\n if (profileId?.trim()) {\n throw new Error(\n `Missing Search Console site URL. profileId \"${profileId}\" was provided, but business-data based site resolution is not implemented yet. Pass siteUrl explicitly or configure Search Console site resolution first.`\n );\n }\n\n throw new Error(\n \"Missing Search Console site URL. Pass siteUrl explicitly or first call gsc_list_accessible_sites to discover available properties.\"\n );\n}\n\nexport function inferSearchConsolePropertyType(\n siteUrl: string\n): \"domain\" | \"url_prefix\" | \"unknown\" {\n if (siteUrl.startsWith(\"sc-domain:\")) {\n return \"domain\";\n }\n\n if (/^https?:\\/\\//.test(siteUrl)) {\n return \"url_prefix\";\n }\n\n return \"unknown\";\n}\n\nexport function normalizePermissionLevel(permissionLevel?: string): string | undefined {\n return permissionLevel?.trim().toLowerCase() || undefined;\n}\n\nexport function normalizeSearchConsoleSiteEntry(siteEntry: SearchConsoleSiteEntry) {\n return {\n site_url: siteEntry.siteUrl,\n property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),\n permission_level: normalizePermissionLevel(siteEntry.permissionLevel),\n };\n}\n\nexport function toPercent(numerator: number, denominator: number): number {\n if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {\n return 0;\n }\n\n return (numerator / denominator) * 100;\n}\n\nexport function round(value: number, decimals = 2): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\nexport function normalizeSearchConsoleRow(\n row: SearchConsoleSearchAnalyticsRow,\n dimensions: readonly string[] = []\n) {\n const clicks = row.clicks ?? 0;\n const impressions = row.impressions ?? 0;\n const ctr = typeof row.ctr === \"number\" ? row.ctr * 100 : toPercent(clicks, impressions);\n const keys = row.keys ?? [];\n\n return {\n keys,\n dimensions: dimensions.reduce<Record<string, string>>((accumulator, dimension, index) => {\n accumulator[dimension] = keys[index] ?? \"\";\n return accumulator;\n }, {}),\n clicks: round(clicks, 2),\n impressions: round(impressions, 2),\n ctr_percent: round(ctr, 2),\n position: round(row.position ?? 0, 2),\n };\n}\n\nexport function buildSearchConsoleQueryBody(input: {\n startDate: string;\n endDate: string;\n dimensions?: string[];\n searchType?: string;\n dimensionFilters?: SearchConsoleDimensionFilter[];\n aggregationType?: string;\n rowLimit?: number;\n startRow?: number;\n}): SearchConsoleSearchAnalyticsRequest {\n const dimensionFilterGroups: SearchConsoleDimensionFilterGroup[] | undefined =\n input.dimensionFilters && input.dimensionFilters.length > 0\n ? [\n {\n groupType: \"and\",\n filters: input.dimensionFilters,\n },\n ]\n : undefined;\n\n return {\n startDate: input.startDate,\n endDate: input.endDate,\n dimensions: input.dimensions,\n type: input.searchType ?? \"web\",\n dimensionFilterGroups,\n aggregationType: input.aggregationType,\n rowLimit: input.rowLimit,\n startRow: input.startRow,\n };\n}\n\nexport function buildPaginationMetadata(input: {\n startRow?: number;\n rowLimit?: number;\n returnedRows: number;\n}) {\n const startRow = input.startRow ?? 0;\n const rowLimit = input.rowLimit;\n const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;\n const nextStartRow = isPartial ? startRow + input.returnedRows : undefined;\n\n return {\n is_partial: isPartial,\n start_row: startRow,\n row_limit: rowLimit,\n returned_rows: input.returnedRows,\n next_start_row: nextStartRow,\n continuation_instructions: nextStartRow\n ? \"This Search Console response may be truncated at row_limit. If you need additional rows, call the same tool again with startRow set to next_start_row.\"\n : undefined,\n };\n}\n\nexport function parseBrandTerms(brandTerms?: string[]): string[] {\n return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);\n}\n\nexport function isBrandedQuery(query: string, brandTerms?: string[]): boolean {\n const normalizedQuery = query.trim().toLowerCase();\n const normalizedBrandTerms = parseBrandTerms(brandTerms);\n\n if (!normalizedQuery || normalizedBrandTerms.length === 0) {\n return false;\n }\n\n return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));\n}\n\nexport function classifyBrandLabel(query: string, brandTerms?: string[]): \"brand\" | \"non_brand\" {\n return isBrandedQuery(query, brandTerms) ? \"brand\" : \"non_brand\";\n}\n\nexport function computeChangePercent(currentValue: number, previousValue: number): number | null {\n if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {\n return null;\n }\n\n if (previousValue === 0) {\n if (currentValue === 0) {\n return 0;\n }\n\n return null;\n }\n\n return round(((currentValue - previousValue) / previousValue) * 100, 2);\n}\n\nexport function sumSearchConsoleRows(\n rows: Array<ReturnType<typeof normalizeSearchConsoleRow>>\n): {\n clicks: number;\n impressions: number;\n ctrPercent: number;\n averagePosition: number;\n} {\n const totals = rows.reduce(\n (accumulator, row) => {\n accumulator.clicks += row.clicks;\n accumulator.impressions += row.impressions;\n accumulator.weightedPosition += row.position * row.impressions;\n return accumulator;\n },\n {\n clicks: 0,\n impressions: 0,\n weightedPosition: 0,\n }\n );\n\n return {\n clicks: round(totals.clicks, 2),\n impressions: round(totals.impressions, 2),\n ctrPercent: round(toPercent(totals.clicks, totals.impressions), 2),\n averagePosition: totals.impressions > 0 ? round(totals.weightedPosition / totals.impressions, 2) : 0,\n };\n}\n\nexport function buildComparisonMap(\n rows: Array<ReturnType<typeof normalizeSearchConsoleRow>>,\n keyBuilder: (row: ReturnType<typeof normalizeSearchConsoleRow>) => string\n) {\n return new Map(rows.map((row) => [keyBuilder(row), row]));\n}\n\nexport function tokenizeQuery(query: string): string[] {\n return query\n .toLowerCase()\n .split(/[^a-z0-9\u00E1\u00E9\u00ED\u00F3\u00FA\u00FC\u00F1]+/i)\n .map((token) => token.trim())\n .filter(Boolean);\n}\n\nexport function looksLikeCommercialQuery(query: string): boolean {\n const tokens = tokenizeQuery(query);\n const commercialTerms = new Set([\n \"comprar\",\n \"precio\",\n \"precios\",\n \"cuanto\",\n \"oferta\",\n \"ofertas\",\n \"envio\",\n \"shop\",\n \"tienda\",\n \"modelo\",\n \"medida\",\n \"ml\",\n \"cm\",\n \"kg\",\n ]);\n\n return tokens.some((token) => commercialTerms.has(token));\n}\n\nexport function looksLikeProductQuery(query: string, productTerms?: string[]): boolean {\n const normalizedProductTerms = parseBrandTerms(productTerms);\n const normalizedQuery = query.toLowerCase();\n\n if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {\n return true;\n }\n\n return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;\n}\n\nexport function classifyQuickWinType(position: number, ctrPercent: number): \"ctr\" | \"position\" {\n if (position <= 8 && ctrPercent <= 3) {\n return \"ctr\";\n }\n\n return \"position\";\n}\n\nexport function scoreHighImpressionLowClickOpportunity(input: {\n impressions: number;\n ctrPercent: number;\n position: number;\n}): number {\n const impressionScore = Math.min(input.impressions / 100, 60);\n const ctrPenalty = Math.max(0, 15 - input.ctrPercent) * 2;\n const positionBonus = input.position > 0 && input.position <= 20 ? Math.max(0, 20 - input.position) : 0;\n\n return round(impressionScore + ctrPenalty + positionBonus, 2);\n}\n\nexport function scoreQuickWinOpportunity(input: {\n impressions: number;\n ctrPercent: number;\n position: number;\n}): number {\n const impressionWeight = Math.min(input.impressions / 150, 50);\n const ctrWeight = Math.max(0, 10 - input.ctrPercent) * 3;\n const positionWeight = input.position > 0 && input.position <= 20 ? Math.max(0, 18 - input.position) * 1.8 : 0;\n\n return round(impressionWeight + ctrWeight + positionWeight, 2);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { refreshGoogleAccessToken } from "./oauth.js";
|
|
2
|
+
const GA4_DATA_BASE_URL = "https://analyticsdata.googleapis.com/v1beta";
|
|
3
|
+
const GA4_ADMIN_BASE_URL = "https://analyticsadmin.googleapis.com/v1alpha";
|
|
4
|
+
async function googleApiFetch(url, init) {
|
|
5
|
+
const accessToken = await refreshGoogleAccessToken();
|
|
6
|
+
const response = await fetch(url, {
|
|
7
|
+
...init,
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${accessToken.accessToken}`,
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
...init?.headers ?? {}
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
const payload = await response.json().catch(() => ({}));
|
|
16
|
+
const message = payload.error?.message ?? payload.error?.status ?? `Google API request failed with status ${response.status}`;
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
return await response.json();
|
|
20
|
+
}
|
|
21
|
+
async function listAccountSummaries() {
|
|
22
|
+
const payload = await googleApiFetch(
|
|
23
|
+
`${GA4_ADMIN_BASE_URL}/accountSummaries`
|
|
24
|
+
);
|
|
25
|
+
return {
|
|
26
|
+
accountSummaries: payload.accountSummaries ?? [],
|
|
27
|
+
nextPageToken: payload.nextPageToken
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function getMetadata(propertyId) {
|
|
31
|
+
return googleApiFetch(
|
|
32
|
+
`${GA4_DATA_BASE_URL}/properties/${propertyId}/metadata`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
async function runReport(propertyId, body) {
|
|
36
|
+
return googleApiFetch(
|
|
37
|
+
`${GA4_DATA_BASE_URL}/properties/${propertyId}:runReport`,
|
|
38
|
+
{
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: JSON.stringify(body)
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
async function runRealtimeReport(propertyId, body) {
|
|
45
|
+
return googleApiFetch(
|
|
46
|
+
`${GA4_DATA_BASE_URL}/properties/${propertyId}:runRealtimeReport`,
|
|
47
|
+
{
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify(body)
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
async function checkCompatibility(propertyId, body) {
|
|
54
|
+
return googleApiFetch(
|
|
55
|
+
`${GA4_DATA_BASE_URL}/properties/${propertyId}:checkCompatibility`,
|
|
56
|
+
{
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: JSON.stringify(body)
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
checkCompatibility,
|
|
64
|
+
getMetadata,
|
|
65
|
+
listAccountSummaries,
|
|
66
|
+
runRealtimeReport,
|
|
67
|
+
runReport
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=ga4-client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/analytics/ga4-client.ts"],
|
|
4
|
+
"sourcesContent": ["import { refreshGoogleAccessToken } from \"./oauth.js\";\n\nconst GA4_DATA_BASE_URL = \"https://analyticsdata.googleapis.com/v1beta\";\nconst GA4_ADMIN_BASE_URL = \"https://analyticsadmin.googleapis.com/v1alpha\";\n\nexport interface Ga4PropertySummary {\n property: string;\n displayName: string;\n propertyType?: string;\n parent?: string;\n}\n\nexport interface Ga4AccountSummary {\n name: string;\n account: string;\n displayName: string;\n propertySummaries?: Ga4PropertySummary[];\n}\n\nexport interface Ga4AccountSummariesResponse {\n accountSummaries?: Ga4AccountSummary[];\n nextPageToken?: string;\n}\n\nexport interface ListAccountSummariesResult {\n accountSummaries: Ga4AccountSummary[];\n nextPageToken?: string;\n}\n\nexport interface Ga4MetadataItem {\n apiName: string;\n uiName?: string;\n description?: string;\n category?: string;\n type?: string;\n customDefinition?: boolean;\n deprecatedApiNames?: string[];\n}\n\nexport interface Ga4CompatibilityItem {\n compatibility: string;\n dimensionMetadata?: Ga4MetadataItem;\n metricMetadata?: Ga4MetadataItem;\n}\n\nexport interface Ga4MetadataResponse {\n name: string;\n dimensions?: Ga4MetadataItem[];\n metrics?: Ga4MetadataItem[];\n comparisons?: Ga4MetadataItem[];\n}\n\nexport interface Ga4MetricHeader {\n name: string;\n type?: string;\n}\n\nexport interface Ga4DimensionHeader {\n name: string;\n}\n\nexport interface Ga4DimensionValue {\n value: string;\n}\n\nexport interface Ga4MetricValue {\n value: string;\n}\n\nexport interface Ga4ReportRow {\n dimensionValues?: Ga4DimensionValue[];\n metricValues?: Ga4MetricValue[];\n}\n\nexport interface Ga4PropertyQuotaBucket {\n consumed?: number;\n remaining?: number;\n}\n\nexport interface Ga4PropertyQuota {\n tokensPerDay?: Ga4PropertyQuotaBucket;\n tokensPerHour?: Ga4PropertyQuotaBucket;\n concurrentRequests?: Ga4PropertyQuotaBucket;\n serverErrorsPerProjectPerHour?: Ga4PropertyQuotaBucket;\n potentiallyThresholdedRequestsPerHour?: Ga4PropertyQuotaBucket;\n tokensPerProjectPerHour?: Ga4PropertyQuotaBucket;\n}\n\nexport interface Ga4ReportResponse {\n dimensionHeaders?: Ga4DimensionHeader[];\n metricHeaders?: Ga4MetricHeader[];\n rows?: Ga4ReportRow[];\n rowCount?: number;\n metadata?: {\n currencyCode?: string;\n timeZone?: string;\n [key: string]: unknown;\n };\n propertyQuota?: Ga4PropertyQuota;\n kind?: string;\n}\n\nexport interface Ga4RealtimeReportResponse {\n dimensionHeaders?: Ga4DimensionHeader[];\n metricHeaders?: Ga4MetricHeader[];\n rows?: Ga4ReportRow[];\n rowCount?: number;\n kind?: string;\n}\n\nexport interface Ga4CompatibilityResponse {\n dimensionCompatibilities?: Ga4CompatibilityItem[];\n metricCompatibilities?: Ga4CompatibilityItem[];\n}\n\ninterface GoogleApiErrorPayload {\n error?: {\n code?: number;\n message?: string;\n status?: string;\n };\n}\n\nasync function googleApiFetch<T>(url: string, init?: RequestInit): Promise<T> {\n const accessToken = await refreshGoogleAccessToken();\n\n const response = await fetch(url, {\n ...init,\n headers: {\n Authorization: `Bearer ${accessToken.accessToken}`,\n \"Content-Type\": \"application/json\",\n ...(init?.headers ?? {}),\n },\n });\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => ({}))) as GoogleApiErrorPayload;\n const message =\n payload.error?.message ??\n payload.error?.status ??\n `Google API request failed with status ${response.status}`;\n\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\nexport async function listAccountSummaries(): Promise<ListAccountSummariesResult> {\n const payload = await googleApiFetch<Ga4AccountSummariesResponse>(\n `${GA4_ADMIN_BASE_URL}/accountSummaries`\n );\n\n return {\n accountSummaries: payload.accountSummaries ?? [],\n nextPageToken: payload.nextPageToken,\n };\n}\n\nexport async function getMetadata(propertyId: string): Promise<Ga4MetadataResponse> {\n return googleApiFetch<Ga4MetadataResponse>(\n `${GA4_DATA_BASE_URL}/properties/${propertyId}/metadata`\n );\n}\n\nexport async function runReport(\n propertyId: string,\n body: Record<string, unknown>\n): Promise<Ga4ReportResponse> {\n return googleApiFetch<Ga4ReportResponse>(\n `${GA4_DATA_BASE_URL}/properties/${propertyId}:runReport`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n }\n );\n}\n\nexport async function runRealtimeReport(\n propertyId: string,\n body: Record<string, unknown>\n): Promise<Ga4RealtimeReportResponse> {\n return googleApiFetch<Ga4RealtimeReportResponse>(\n `${GA4_DATA_BASE_URL}/properties/${propertyId}:runRealtimeReport`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n }\n );\n}\n\nexport async function checkCompatibility(\n propertyId: string,\n body: Record<string, unknown>\n): Promise<Ga4CompatibilityResponse> {\n return googleApiFetch<Ga4CompatibilityResponse>(\n `${GA4_DATA_BASE_URL}/properties/${propertyId}:checkCompatibility`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n }\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gCAAgC;AAEzC,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;AAwH3B,eAAe,eAAkB,KAAa,MAAgC;AAC5E,QAAM,cAAc,MAAM,yBAAyB;AAEnD,QAAM,WAAW,MAAM,MAAM,KAAK;AAAA,IAChC,GAAG;AAAA,IACH,SAAS;AAAA,MACP,eAAe,UAAU,YAAY,WAAW;AAAA,MAChD,gBAAgB;AAAA,MAChB,GAAI,MAAM,WAAW,CAAC;AAAA,IACxB;AAAA,EACF,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACvD,UAAM,UACJ,QAAQ,OAAO,WACf,QAAQ,OAAO,UACf,yCAAyC,SAAS,MAAM;AAE1D,UAAM,IAAI,MAAM,OAAO;AAAA,EACzB;AAEA,SAAQ,MAAM,SAAS,KAAK;AAC9B;AAEA,eAAsB,uBAA4D;AAChF,QAAM,UAAU,MAAM;AAAA,IACpB,GAAG,kBAAkB;AAAA,EACvB;AAEA,SAAO;AAAA,IACL,kBAAkB,QAAQ,oBAAoB,CAAC;AAAA,IAC/C,eAAe,QAAQ;AAAA,EACzB;AACF;AAEA,eAAsB,YAAY,YAAkD;AAClF,SAAO;AAAA,IACL,GAAG,iBAAiB,eAAe,UAAU;AAAA,EAC/C;AACF;AAEA,eAAsB,UACpB,YACA,MAC4B;AAC5B,SAAO;AAAA,IACL,GAAG,iBAAiB,eAAe,UAAU;AAAA,IAC7C;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,eAAsB,kBACpB,YACA,MACoC;AACpC,SAAO;AAAA,IACL,GAAG,iBAAiB,eAAe,UAAU;AAAA,IAC7C;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,eAAsB,mBACpB,YACA,MACmC;AACnC,SAAO;AAAA,IACL,GAAG,iBAAiB,eAAe,UAAU;AAAA,IAC7C;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { googleOAuthConfig } from "../../config/google.js";
|
|
2
|
+
async function refreshGoogleAccessToken(credentials = googleOAuthConfig) {
|
|
3
|
+
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: {
|
|
6
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7
|
+
},
|
|
8
|
+
body: new URLSearchParams({
|
|
9
|
+
client_id: credentials.clientId,
|
|
10
|
+
client_secret: credentials.clientSecret,
|
|
11
|
+
refresh_token: credentials.refreshToken,
|
|
12
|
+
grant_type: "refresh_token"
|
|
13
|
+
})
|
|
14
|
+
});
|
|
15
|
+
const payload = await response.json();
|
|
16
|
+
if (!response.ok || !payload.access_token || !payload.expires_in || !payload.token_type) {
|
|
17
|
+
const errorMessage = payload.error_description ?? payload.error ?? `Unexpected Google OAuth response (${response.status})`;
|
|
18
|
+
throw new Error(`Failed to refresh Google access token: ${errorMessage}`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
accessToken: payload.access_token,
|
|
22
|
+
expiresIn: payload.expires_in,
|
|
23
|
+
tokenType: payload.token_type,
|
|
24
|
+
scope: payload.scope
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
refreshGoogleAccessToken
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/analytics/oauth.ts"],
|
|
4
|
+
"sourcesContent": ["import { googleOAuthConfig, type GoogleOAuthConfig } from \"../../config/google.js\";\n\nexport interface GoogleAccessTokenResponse {\n accessToken: string;\n expiresIn: number;\n tokenType: string;\n scope?: string;\n}\n\ninterface GoogleTokenApiResponse {\n access_token?: string;\n expires_in?: number;\n token_type?: string;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n\nexport async function refreshGoogleAccessToken(\n credentials: GoogleOAuthConfig = googleOAuthConfig\n): Promise<GoogleAccessTokenResponse> {\n const response = await fetch(\"https://oauth2.googleapis.com/token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n refresh_token: credentials.refreshToken,\n grant_type: \"refresh_token\",\n }),\n });\n\n const payload = (await response.json()) as GoogleTokenApiResponse;\n\n if (!response.ok || !payload.access_token || !payload.expires_in || !payload.token_type) {\n const errorMessage =\n payload.error_description ??\n payload.error ??\n `Unexpected Google OAuth response (${response.status})`;\n\n throw new Error(`Failed to refresh Google access token: ${errorMessage}`);\n }\n\n return {\n accessToken: payload.access_token,\n expiresIn: payload.expires_in,\n tokenType: payload.token_type,\n scope: payload.scope,\n };\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAiD;AAkB1D,eAAsB,yBACpB,cAAiC,mBACG;AACpC,QAAM,WAAW,MAAM,MAAM,uCAAuC;AAAA,IAClE,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,IAClB;AAAA,IACA,MAAM,IAAI,gBAAgB;AAAA,MACxB,WAAW,YAAY;AAAA,MACvB,eAAe,YAAY;AAAA,MAC3B,eAAe,YAAY;AAAA,MAC3B,YAAY;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AAED,QAAM,UAAW,MAAM,SAAS,KAAK;AAErC,MAAI,CAAC,SAAS,MAAM,CAAC,QAAQ,gBAAgB,CAAC,QAAQ,cAAc,CAAC,QAAQ,YAAY;AACvF,UAAM,eACJ,QAAQ,qBACR,QAAQ,SACR,qCAAqC,SAAS,MAAM;AAEtD,UAAM,IAAI,MAAM,0CAA0C,YAAY,EAAE;AAAA,EAC1E;AAEA,SAAO;AAAA,IACL,aAAa,QAAQ;AAAA,IACrB,WAAW,QAAQ;AAAA,IACnB,WAAW,QAAQ;AAAA,IACnB,OAAO,QAAQ;AAAA,EACjB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { refreshGoogleAccessToken } from "../analytics/oauth.js";
|
|
2
|
+
const GOOGLE_ADS_BASE_URL = "https://googleads.googleapis.com/v22";
|
|
3
|
+
async function googleAdsFetch(path, developerToken, options = {}) {
|
|
4
|
+
const accessToken = await refreshGoogleAccessToken();
|
|
5
|
+
const response = await fetch(`${GOOGLE_ADS_BASE_URL}${path}`, {
|
|
6
|
+
method: options.method ?? "POST",
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${accessToken.accessToken}`,
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
"developer-token": developerToken,
|
|
11
|
+
...options.loginCustomerId ? { "login-customer-id": options.loginCustomerId } : {}
|
|
12
|
+
},
|
|
13
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
const payload = await response.json().catch(() => ({}));
|
|
17
|
+
const detailedMessage = payload.error?.details?.flatMap((detail) => detail.errors ?? []).map((error) => error.message).filter(Boolean).join("; ");
|
|
18
|
+
const message = detailedMessage || payload.error?.message || payload.error?.status || `Google Ads API request failed with status ${response.status}`;
|
|
19
|
+
throw new Error(message);
|
|
20
|
+
}
|
|
21
|
+
return await response.json();
|
|
22
|
+
}
|
|
23
|
+
async function listAccessibleCustomers(developerToken) {
|
|
24
|
+
const payload = await googleAdsFetch(
|
|
25
|
+
"/customers:listAccessibleCustomers",
|
|
26
|
+
developerToken,
|
|
27
|
+
{ method: "GET" }
|
|
28
|
+
);
|
|
29
|
+
return payload.resourceNames?.map((value) => value.replace("customers/", "")) ?? [];
|
|
30
|
+
}
|
|
31
|
+
async function searchGoogleAds(customerId, query, developerToken, loginCustomerId) {
|
|
32
|
+
const batches = await googleAdsFetch(
|
|
33
|
+
`/customers/${customerId}/googleAds:searchStream`,
|
|
34
|
+
developerToken,
|
|
35
|
+
{
|
|
36
|
+
customerId,
|
|
37
|
+
loginCustomerId,
|
|
38
|
+
body: { query }
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
const rows = batches.flatMap((batch) => batch.results ?? []);
|
|
42
|
+
const lastBatch = batches.at(-1);
|
|
43
|
+
return {
|
|
44
|
+
rows,
|
|
45
|
+
summaryRow: lastBatch?.summaryRow,
|
|
46
|
+
requestId: lastBatch?.requestId,
|
|
47
|
+
fieldMask: lastBatch?.fieldMask
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
listAccessibleCustomers,
|
|
52
|
+
searchGoogleAds
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=google-ads-client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/google-ads/google-ads-client.ts"],
|
|
4
|
+
"sourcesContent": ["import { refreshGoogleAccessToken } from \"../analytics/oauth.js\";\n\nexport interface GoogleAdsCustomer {\n id?: string;\n descriptiveName?: string;\n currencyCode?: string;\n timeZone?: string;\n manager?: boolean;\n testAccount?: boolean;\n}\n\nexport interface GoogleAdsCustomerClient {\n clientCustomer?: string;\n descriptiveName?: string;\n currencyCode?: string;\n timeZone?: string;\n manager?: boolean;\n testAccount?: boolean;\n status?: string;\n}\n\nexport interface GoogleAdsCampaign {\n id?: string;\n name?: string;\n status?: string;\n advertisingChannelType?: string;\n}\n\nexport interface GoogleAdsAdGroup {\n id?: string;\n name?: string;\n}\n\nexport interface GoogleAdsSearchTermView {\n searchTerm?: string;\n}\n\nexport interface GoogleAdsSegments {\n date?: string;\n week?: string;\n}\n\nexport interface GoogleAdsMetrics {\n impressions?: string;\n clicks?: string;\n costMicros?: string;\n conversions?: string;\n conversionsValue?: string;\n ctr?: number | string;\n averageCpc?: string;\n}\n\nexport interface GoogleAdsSearchRow {\n customer?: GoogleAdsCustomer;\n customerClient?: GoogleAdsCustomerClient;\n campaign?: GoogleAdsCampaign;\n adGroup?: GoogleAdsAdGroup;\n searchTermView?: GoogleAdsSearchTermView;\n segments?: GoogleAdsSegments;\n metrics?: GoogleAdsMetrics;\n}\n\nexport type GoogleAdsMetricValue = string | number | undefined;\n\ninterface GoogleAdsListAccessibleCustomersResponse {\n resourceNames?: string[];\n}\n\ninterface GoogleAdsSearchStreamBatch {\n results?: GoogleAdsSearchRow[];\n summaryRow?: GoogleAdsSearchRow;\n fieldMask?: string;\n requestId?: string;\n}\n\ninterface GoogleAdsErrorPayload {\n error?: {\n code?: number;\n message?: string;\n status?: string;\n details?: Array<{\n errors?: Array<{\n errorCode?: Record<string, string>;\n message?: string;\n }>;\n }>;\n };\n}\n\nexport interface GoogleAdsSearchResult {\n rows: GoogleAdsSearchRow[];\n summaryRow?: GoogleAdsSearchRow;\n requestId?: string;\n fieldMask?: string;\n}\n\ninterface GoogleAdsRequestOptions {\n customerId?: string;\n loginCustomerId?: string;\n body?: Record<string, unknown>;\n method?: \"GET\" | \"POST\";\n}\n\nconst GOOGLE_ADS_BASE_URL = \"https://googleads.googleapis.com/v22\";\n\nasync function googleAdsFetch<T>(\n path: string,\n developerToken: string,\n options: GoogleAdsRequestOptions = {}\n): Promise<T> {\n const accessToken = await refreshGoogleAccessToken();\n const response = await fetch(`${GOOGLE_ADS_BASE_URL}${path}`, {\n method: options.method ?? \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken.accessToken}`,\n \"Content-Type\": \"application/json\",\n \"developer-token\": developerToken,\n ...(options.loginCustomerId\n ? { \"login-customer-id\": options.loginCustomerId }\n : {}),\n },\n body: options.body ? JSON.stringify(options.body) : undefined,\n });\n\n if (!response.ok) {\n const payload = (await response.json().catch(() => ({}))) as GoogleAdsErrorPayload;\n const detailedMessage = payload.error?.details\n ?.flatMap((detail) => detail.errors ?? [])\n .map((error) => error.message)\n .filter(Boolean)\n .join(\"; \");\n const message =\n detailedMessage ||\n payload.error?.message ||\n payload.error?.status ||\n `Google Ads API request failed with status ${response.status}`;\n\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\nexport async function listAccessibleCustomers(\n developerToken: string\n): Promise<string[]> {\n const payload = await googleAdsFetch<GoogleAdsListAccessibleCustomersResponse>(\n \"/customers:listAccessibleCustomers\",\n developerToken,\n { method: \"GET\" }\n );\n\n return payload.resourceNames?.map((value) => value.replace(\"customers/\", \"\")) ?? [];\n}\n\nexport async function searchGoogleAds(\n customerId: string,\n query: string,\n developerToken: string,\n loginCustomerId?: string\n): Promise<GoogleAdsSearchResult> {\n const batches = await googleAdsFetch<GoogleAdsSearchStreamBatch[]>(\n `/customers/${customerId}/googleAds:searchStream`,\n developerToken,\n {\n customerId,\n loginCustomerId,\n body: { query },\n }\n );\n\n const rows = batches.flatMap((batch) => batch.results ?? []);\n const lastBatch = batches.at(-1);\n\n return {\n rows,\n summaryRow: lastBatch?.summaryRow,\n requestId: lastBatch?.requestId,\n fieldMask: lastBatch?.fieldMask,\n };\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gCAAgC;AAuGzC,MAAM,sBAAsB;AAE5B,eAAe,eACb,MACA,gBACA,UAAmC,CAAC,GACxB;AACZ,QAAM,cAAc,MAAM,yBAAyB;AACnD,QAAM,WAAW,MAAM,MAAM,GAAG,mBAAmB,GAAG,IAAI,IAAI;AAAA,IAC5D,QAAQ,QAAQ,UAAU;AAAA,IAC1B,SAAS;AAAA,MACP,eAAe,UAAU,YAAY,WAAW;AAAA,MAChD,gBAAgB;AAAA,MAChB,mBAAmB;AAAA,MACnB,GAAI,QAAQ,kBACR,EAAE,qBAAqB,QAAQ,gBAAgB,IAC/C,CAAC;AAAA,IACP;AAAA,IACA,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;AAAA,EACtD,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,UAAW,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACvD,UAAM,kBAAkB,QAAQ,OAAO,SACnC,QAAQ,CAAC,WAAW,OAAO,UAAU,CAAC,CAAC,EACxC,IAAI,CAAC,UAAU,MAAM,OAAO,EAC5B,OAAO,OAAO,EACd,KAAK,IAAI;AACZ,UAAM,UACJ,mBACA,QAAQ,OAAO,WACf,QAAQ,OAAO,UACf,6CAA6C,SAAS,MAAM;AAE9D,UAAM,IAAI,MAAM,OAAO;AAAA,EACzB;AAEA,SAAQ,MAAM,SAAS,KAAK;AAC9B;AAEA,eAAsB,wBACpB,gBACmB;AACnB,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA,EAAE,QAAQ,MAAM;AAAA,EAClB;AAEA,SAAO,QAAQ,eAAe,IAAI,CAAC,UAAU,MAAM,QAAQ,cAAc,EAAE,CAAC,KAAK,CAAC;AACpF;AAEA,eAAsB,gBACpB,YACA,OACA,gBACA,iBACgC;AAChC,QAAM,UAAU,MAAM;AAAA,IACpB,cAAc,UAAU;AAAA,IACxB;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA,MAAM,EAAE,MAAM;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,QAAQ,CAAC,UAAU,MAAM,WAAW,CAAC,CAAC;AAC3D,QAAM,YAAY,QAAQ,GAAG,EAAE;AAE/B,SAAO;AAAA,IACL;AAAA,IACA,YAAY,WAAW;AAAA,IACvB,WAAW,WAAW;AAAA,IACtB,WAAW,WAAW;AAAA,EACxB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|