@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,45 @@
|
|
|
1
|
+
import { refreshGoogleAccessToken } from "../analytics/oauth.js";
|
|
2
|
+
const SEARCH_CONSOLE_BASE_URL = "https://www.googleapis.com/webmasters/v3";
|
|
3
|
+
async function googleApiFetch(url, init) {
|
|
4
|
+
const accessToken = await refreshGoogleAccessToken();
|
|
5
|
+
const response = await fetch(url, {
|
|
6
|
+
...init,
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${accessToken.accessToken}`,
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
...init?.headers ?? {}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const payload = await response.json().catch(() => ({}));
|
|
15
|
+
const message = payload.error?.message ?? payload.error?.status ?? `Google Search Console API request failed with status ${response.status}`;
|
|
16
|
+
const normalizedMessage = message.toLowerCase();
|
|
17
|
+
if (response.status === 403 && normalizedMessage.includes("insufficient authentication scopes")) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Request had insufficient authentication scopes. Reconnect the Google OAuth credentials with Search Console scope https://www.googleapis.com/auth/webmasters.readonly or https://www.googleapis.com/auth/webmasters."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
throw new Error(message);
|
|
23
|
+
}
|
|
24
|
+
return await response.json();
|
|
25
|
+
}
|
|
26
|
+
async function listSearchConsoleSites() {
|
|
27
|
+
const payload = await googleApiFetch(
|
|
28
|
+
`${SEARCH_CONSOLE_BASE_URL}/sites`
|
|
29
|
+
);
|
|
30
|
+
return payload.siteEntry ?? [];
|
|
31
|
+
}
|
|
32
|
+
async function querySearchConsolePerformance(siteUrl, body) {
|
|
33
|
+
return googleApiFetch(
|
|
34
|
+
`${SEARCH_CONSOLE_BASE_URL}/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`,
|
|
35
|
+
{
|
|
36
|
+
method: "POST",
|
|
37
|
+
body: JSON.stringify(body)
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
listSearchConsoleSites,
|
|
43
|
+
querySearchConsolePerformance
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=search-console-client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/search-console/search-console-client.ts"],
|
|
4
|
+
"sourcesContent": ["import { refreshGoogleAccessToken } from \"../analytics/oauth.js\";\n\nconst SEARCH_CONSOLE_BASE_URL = \"https://www.googleapis.com/webmasters/v3\";\n\nexport interface SearchConsoleSiteEntry {\n siteUrl: string;\n permissionLevel?: string;\n}\n\nexport interface SearchConsoleSitesListResponse {\n siteEntry?: SearchConsoleSiteEntry[];\n}\n\nexport interface SearchConsoleDimensionFilter {\n dimension: string;\n operator: string;\n expression: string;\n}\n\nexport interface SearchConsoleDimensionFilterGroup {\n groupType?: string;\n filters: SearchConsoleDimensionFilter[];\n}\n\nexport interface SearchConsoleSearchAnalyticsRequest {\n startDate: string;\n endDate: string;\n dimensions?: string[];\n type?: string;\n dimensionFilterGroups?: SearchConsoleDimensionFilterGroup[];\n aggregationType?: string;\n rowLimit?: number;\n startRow?: number;\n}\n\nexport interface SearchConsoleSearchAnalyticsRow {\n keys?: string[];\n clicks?: number;\n impressions?: number;\n ctr?: number;\n position?: number;\n}\n\nexport interface SearchConsoleSearchAnalyticsResponse {\n rows?: SearchConsoleSearchAnalyticsRow[];\n responseAggregationType?: string;\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 Search Console API request failed with status ${response.status}`;\n\n const normalizedMessage = message.toLowerCase();\n if (\n response.status === 403 &&\n normalizedMessage.includes(\"insufficient authentication scopes\")\n ) {\n throw new Error(\n \"Request had insufficient authentication scopes. Reconnect the Google OAuth credentials with Search Console scope https://www.googleapis.com/auth/webmasters.readonly or https://www.googleapis.com/auth/webmasters.\"\n );\n }\n\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\nexport async function listSearchConsoleSites(): Promise<SearchConsoleSiteEntry[]> {\n const payload = await googleApiFetch<SearchConsoleSitesListResponse>(\n `${SEARCH_CONSOLE_BASE_URL}/sites`\n );\n\n return payload.siteEntry ?? [];\n}\n\nexport async function querySearchConsolePerformance(\n siteUrl: string,\n body: SearchConsoleSearchAnalyticsRequest\n): Promise<SearchConsoleSearchAnalyticsResponse> {\n return googleApiFetch<SearchConsoleSearchAnalyticsResponse>(\n `${SEARCH_CONSOLE_BASE_URL}/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`,\n {\n method: \"POST\",\n body: JSON.stringify(body),\n }\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gCAAgC;AAEzC,MAAM,0BAA0B;AAsDhC,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,wDAAwD,SAAS,MAAM;AAEzE,UAAM,oBAAoB,QAAQ,YAAY;AAC9C,QACE,SAAS,WAAW,OACpB,kBAAkB,SAAS,oCAAoC,GAC/D;AACA,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,IAAI,MAAM,OAAO;AAAA,EACzB;AAEA,SAAQ,MAAM,SAAS,KAAK;AAC9B;AAEA,eAAsB,yBAA4D;AAChF,QAAM,UAAU,MAAM;AAAA,IACpB,GAAG,uBAAuB;AAAA,EAC5B;AAEA,SAAO,QAAQ,aAAa,CAAC;AAC/B;AAEA,eAAsB,8BACpB,SACA,MAC+C;AAC/C,SAAO;AAAA,IACL,GAAG,uBAAuB,UAAU,mBAAmB,OAAO,CAAC;AAAA,IAC/D;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { vtexConfig } from "../../config/vtex.js";
|
|
3
|
+
function createVtexClient(baseURL) {
|
|
4
|
+
const client = axios.create({
|
|
5
|
+
baseURL,
|
|
6
|
+
headers: {
|
|
7
|
+
"X-VTEX-API-AppKey": vtexConfig.apiKey,
|
|
8
|
+
"X-VTEX-API-AppToken": vtexConfig.apiToken,
|
|
9
|
+
Accept: "application/json",
|
|
10
|
+
"Content-Type": "application/json"
|
|
11
|
+
},
|
|
12
|
+
timeout: 3e4
|
|
13
|
+
});
|
|
14
|
+
client.interceptors.response.use(
|
|
15
|
+
(response) => response,
|
|
16
|
+
(error) => {
|
|
17
|
+
const method = error.config?.method?.toUpperCase() ?? "UNKNOWN";
|
|
18
|
+
const url = error.config?.url ?? "UNKNOWN_URL";
|
|
19
|
+
const status = error.response?.status ?? "NO_STATUS";
|
|
20
|
+
console.error(`[VTEX API ERROR] ${method} ${url} -> ${status}`);
|
|
21
|
+
if (error.response?.data) {
|
|
22
|
+
console.error("[VTEX API ERROR BODY]", error.response.data);
|
|
23
|
+
}
|
|
24
|
+
return Promise.reject(error);
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
29
|
+
const vtexApi = createVtexClient(vtexConfig.baseUrl);
|
|
30
|
+
const vtexPricingApi = createVtexClient(vtexConfig.pricingBaseUrl);
|
|
31
|
+
function formatVtexError(error, fallback = "Unexpected VTEX API error") {
|
|
32
|
+
if (axios.isAxiosError(error)) {
|
|
33
|
+
const status = error.response?.status;
|
|
34
|
+
const responseData = error.response?.data;
|
|
35
|
+
const responseMessage = typeof responseData === "object" && responseData !== null && "message" in responseData ? String(responseData.message) : typeof responseData === "string" ? responseData : void 0;
|
|
36
|
+
if (status) {
|
|
37
|
+
return `VTEX API request failed (${status}): ${responseMessage ?? error.message}`;
|
|
38
|
+
}
|
|
39
|
+
return `VTEX API request failed: ${responseMessage ?? error.message}`;
|
|
40
|
+
}
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
return error.message;
|
|
43
|
+
}
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
formatVtexError,
|
|
48
|
+
vtexApi,
|
|
49
|
+
vtexPricingApi
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=vtex-api.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/vtex/vtex-api.ts"],
|
|
4
|
+
"sourcesContent": ["import axios from \"axios\";\nimport type { AxiosError, AxiosInstance } from \"axios\";\n\nimport { vtexConfig } from \"../../config/vtex.js\";\n\nfunction createVtexClient(baseURL: string): AxiosInstance {\n const client = axios.create({\n baseURL,\n headers: {\n \"X-VTEX-API-AppKey\": vtexConfig.apiKey,\n \"X-VTEX-API-AppToken\": vtexConfig.apiToken,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n timeout: 30000,\n });\n\n client.interceptors.response.use(\n (response) => response,\n (error: AxiosError) => {\n const method = error.config?.method?.toUpperCase() ?? \"UNKNOWN\";\n const url = error.config?.url ?? \"UNKNOWN_URL\";\n const status = error.response?.status ?? \"NO_STATUS\";\n\n console.error(`[VTEX API ERROR] ${method} ${url} -> ${status}`);\n\n if (error.response?.data) {\n console.error(\"[VTEX API ERROR BODY]\", error.response.data);\n }\n\n return Promise.reject(error);\n }\n );\n\n return client;\n}\n\nexport const vtexApi = createVtexClient(vtexConfig.baseUrl);\nexport const vtexPricingApi = createVtexClient(vtexConfig.pricingBaseUrl);\n\nexport function formatVtexError(error: unknown, fallback = \"Unexpected VTEX API error\"): string {\n if (axios.isAxiosError(error)) {\n const status = error.response?.status;\n const responseData = error.response?.data;\n const responseMessage =\n typeof responseData === \"object\" && responseData !== null && \"message\" in responseData\n ? String((responseData as { message?: unknown }).message)\n : typeof responseData === \"string\"\n ? responseData\n : undefined;\n\n if (status) {\n return `VTEX API request failed (${status}): ${responseMessage ?? error.message}`;\n }\n\n return `VTEX API request failed: ${responseMessage ?? error.message}`;\n }\n\n if (error instanceof Error) {\n return error.message;\n }\n\n return fallback;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,WAAW;AAGlB,SAAS,kBAAkB;AAE3B,SAAS,iBAAiB,SAAgC;AACxD,QAAM,SAAS,MAAM,OAAO;AAAA,IAC1B;AAAA,IACA,SAAS;AAAA,MACP,qBAAqB,WAAW;AAAA,MAChC,uBAAuB,WAAW;AAAA,MAClC,QAAQ;AAAA,MACR,gBAAgB;AAAA,IAClB;AAAA,IACA,SAAS;AAAA,EACX,CAAC;AAED,SAAO,aAAa,SAAS;AAAA,IAC3B,CAAC,aAAa;AAAA,IACd,CAAC,UAAsB;AACrB,YAAM,SAAS,MAAM,QAAQ,QAAQ,YAAY,KAAK;AACtD,YAAM,MAAM,MAAM,QAAQ,OAAO;AACjC,YAAM,SAAS,MAAM,UAAU,UAAU;AAEzC,cAAQ,MAAM,oBAAoB,MAAM,IAAI,GAAG,OAAO,MAAM,EAAE;AAE9D,UAAI,MAAM,UAAU,MAAM;AACxB,gBAAQ,MAAM,yBAAyB,MAAM,SAAS,IAAI;AAAA,MAC5D;AAEA,aAAO,QAAQ,OAAO,KAAK;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO;AACT;AAEO,MAAM,UAAU,iBAAiB,WAAW,OAAO;AACnD,MAAM,iBAAiB,iBAAiB,WAAW,cAAc;AAEjE,SAAS,gBAAgB,OAAgB,WAAW,6BAAqC;AAC9F,MAAI,MAAM,aAAa,KAAK,GAAG;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,eAAe,MAAM,UAAU;AACrC,UAAM,kBACJ,OAAO,iBAAiB,YAAY,iBAAiB,QAAQ,aAAa,eACtE,OAAQ,aAAuC,OAAO,IACtD,OAAO,iBAAiB,WACtB,eACA;AAER,QAAI,QAAQ;AACV,aAAO,4BAA4B,MAAM,MAAM,mBAAmB,MAAM,OAAO;AAAA,IACjF;AAEA,WAAO,4BAA4B,mBAAmB,MAAM,OAAO;AAAA,EACrE;AAEA,MAAI,iBAAiB,OAAO;AAC1B,WAAO,MAAM;AAAA,EACf;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { vtexApi } from "./vtex-api.js";
|
|
2
|
+
async function getProductOffers(productId) {
|
|
3
|
+
const response = await vtexApi.get(
|
|
4
|
+
`/api/catalog_system/pub/products/offers/${productId}`
|
|
5
|
+
);
|
|
6
|
+
return response.data;
|
|
7
|
+
}
|
|
8
|
+
async function getSkuOffers(productId, skuId) {
|
|
9
|
+
const response = await vtexApi.get(
|
|
10
|
+
`/api/catalog_system/pub/products/offers/${productId}/sku/${skuId}`
|
|
11
|
+
);
|
|
12
|
+
return response.data;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
getProductOffers,
|
|
16
|
+
getSkuOffers
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=vtex-catalog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/vtex/vtex-catalog.ts"],
|
|
4
|
+
"sourcesContent": ["import { vtexApi } from \"./vtex-api.js\";\n\nexport type ProductOffersResponse = Record<string, unknown>[];\n\nexport async function getProductOffers(productId: string): Promise<ProductOffersResponse> {\n const response = await vtexApi.get<ProductOffersResponse>(\n `/api/catalog_system/pub/products/offers/${productId}`\n );\n\n return response.data;\n}\n\nexport async function getSkuOffers(productId: string, skuId: string): Promise<ProductOffersResponse> {\n const response = await vtexApi.get<ProductOffersResponse>(\n `/api/catalog_system/pub/products/offers/${productId}/sku/${skuId}`\n );\n\n return response.data;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAIxB,eAAsB,iBAAiB,WAAmD;AACxF,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,2CAA2C,SAAS;AAAA,EACtD;AAEA,SAAO,SAAS;AAClB;AAEA,eAAsB,aAAa,WAAmB,OAA+C;AACnG,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,2CAA2C,SAAS,QAAQ,KAAK;AAAA,EACnE;AAEA,SAAO,SAAS;AAClB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { vtexApi } from "./vtex-api.js";
|
|
3
|
+
const RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
4
|
+
const DEFAULT_BATCH_CONCURRENCY = 10;
|
|
5
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
6
|
+
const DEFAULT_BASE_RETRY_DELAY_MS = 250;
|
|
7
|
+
async function getInventoryBySku(skuId) {
|
|
8
|
+
const response = await vtexApi.get(`/api/logistics/pvt/inventory/skus/${skuId}`);
|
|
9
|
+
return response.data;
|
|
10
|
+
}
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
setTimeout(resolve, ms);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function parseRetryAfterMs(headerValue) {
|
|
17
|
+
if (Array.isArray(headerValue)) {
|
|
18
|
+
return parseRetryAfterMs(headerValue[0]);
|
|
19
|
+
}
|
|
20
|
+
if (typeof headerValue === "number" && Number.isFinite(headerValue)) {
|
|
21
|
+
return Math.max(0, Math.floor(headerValue * 1e3));
|
|
22
|
+
}
|
|
23
|
+
if (typeof headerValue !== "string") {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
const trimmedValue = headerValue.trim();
|
|
27
|
+
if (!trimmedValue) {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
const seconds = Number(trimmedValue);
|
|
31
|
+
if (Number.isFinite(seconds)) {
|
|
32
|
+
return Math.max(0, Math.floor(seconds * 1e3));
|
|
33
|
+
}
|
|
34
|
+
const absoluteDateMs = Date.parse(trimmedValue);
|
|
35
|
+
if (Number.isNaN(absoluteDateMs)) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
return Math.max(0, absoluteDateMs - Date.now());
|
|
39
|
+
}
|
|
40
|
+
function computeBackoffDelayMs(attempt, baseDelayMs) {
|
|
41
|
+
const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
42
|
+
const jitter = Math.floor(Math.random() * 150);
|
|
43
|
+
return exponential + jitter;
|
|
44
|
+
}
|
|
45
|
+
function isRetryableRequestError(error) {
|
|
46
|
+
if (!axios.isAxiosError(error)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (!error.response?.status) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return RETRYABLE_STATUS_CODES.has(error.response.status);
|
|
53
|
+
}
|
|
54
|
+
function toInventoryFetchError(skuId, source, attempts, retryable) {
|
|
55
|
+
const statusCode = axios.isAxiosError(source) ? source.response?.status : void 0;
|
|
56
|
+
const message = source instanceof Error ? source.message : typeof source === "string" ? source : "Unexpected error while fetching inventory details";
|
|
57
|
+
const error = new Error(message);
|
|
58
|
+
error.skuId = skuId;
|
|
59
|
+
error.statusCode = statusCode;
|
|
60
|
+
error.attempts = attempts;
|
|
61
|
+
error.retryable = retryable;
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
function isInventoryFetchError(error) {
|
|
65
|
+
if (!(error instanceof Error)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return "skuId" in error && "attempts" in error && "retryable" in error && typeof error.skuId === "string";
|
|
69
|
+
}
|
|
70
|
+
async function getInventoryBySkuWithRetry(skuId, options) {
|
|
71
|
+
const maxRetries = Math.max(0, Math.floor(options.maxRetries ?? DEFAULT_MAX_RETRIES));
|
|
72
|
+
const baseRetryDelayMs = Math.max(
|
|
73
|
+
50,
|
|
74
|
+
Math.floor(options.baseRetryDelayMs ?? DEFAULT_BASE_RETRY_DELAY_MS)
|
|
75
|
+
);
|
|
76
|
+
let attempts = 0;
|
|
77
|
+
while (true) {
|
|
78
|
+
attempts += 1;
|
|
79
|
+
try {
|
|
80
|
+
const document = await getInventoryBySku(skuId);
|
|
81
|
+
return { skuId, document };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const retryable = isRetryableRequestError(error);
|
|
84
|
+
if (!retryable || attempts > maxRetries) {
|
|
85
|
+
throw toInventoryFetchError(skuId, error, attempts, retryable);
|
|
86
|
+
}
|
|
87
|
+
const retryAfterHeader = axios.isAxiosError(error) ? error.response?.headers?.["retry-after"] : void 0;
|
|
88
|
+
const retryAfterMs = parseRetryAfterMs(retryAfterHeader);
|
|
89
|
+
const delayMs = retryAfterMs ?? computeBackoffDelayMs(attempts, baseRetryDelayMs);
|
|
90
|
+
await sleep(delayMs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function getInventoryBySkuBatch(skuIds, options = {}) {
|
|
95
|
+
const maxConcurrency = Math.max(
|
|
96
|
+
1,
|
|
97
|
+
Math.floor(options.maxConcurrency ?? DEFAULT_BATCH_CONCURRENCY)
|
|
98
|
+
);
|
|
99
|
+
const successful = [];
|
|
100
|
+
const failed = [];
|
|
101
|
+
for (let start = 0; start < skuIds.length; start += maxConcurrency) {
|
|
102
|
+
const chunk = skuIds.slice(start, start + maxConcurrency);
|
|
103
|
+
const settledChunk = await Promise.allSettled(
|
|
104
|
+
chunk.map((skuId) => getInventoryBySkuWithRetry(skuId, options))
|
|
105
|
+
);
|
|
106
|
+
settledChunk.forEach((result, index) => {
|
|
107
|
+
const skuId = chunk[index];
|
|
108
|
+
if (result.status === "fulfilled") {
|
|
109
|
+
successful.push(result.value);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (isInventoryFetchError(result.reason)) {
|
|
113
|
+
failed.push({
|
|
114
|
+
skuId: result.reason.skuId,
|
|
115
|
+
message: result.reason.message,
|
|
116
|
+
statusCode: result.reason.statusCode,
|
|
117
|
+
attempts: result.reason.attempts,
|
|
118
|
+
retryable: result.reason.retryable
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
failed.push({
|
|
123
|
+
skuId,
|
|
124
|
+
message: result.reason instanceof Error ? result.reason.message : "Unexpected error while fetching inventory details",
|
|
125
|
+
attempts: 1,
|
|
126
|
+
retryable: false
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { successful, failed };
|
|
131
|
+
}
|
|
132
|
+
async function getInventoryByWarehouse(skuId, warehouseId) {
|
|
133
|
+
const response = await vtexApi.get(
|
|
134
|
+
`/api/logistics/pvt/inventory/items/${skuId}/warehouses/${warehouseId}`
|
|
135
|
+
);
|
|
136
|
+
return response.data;
|
|
137
|
+
}
|
|
138
|
+
async function updateInventoryQuantity(skuId, warehouseId, payload) {
|
|
139
|
+
await vtexApi.patch(`/api/logistics/pvt/inventory/skus/${skuId}/warehouses/${warehouseId}/quantity`, payload);
|
|
140
|
+
}
|
|
141
|
+
async function updateInventoryLeadTime(skuId, warehouseId, payload) {
|
|
142
|
+
await vtexApi.patch(`/api/logistics/pvt/inventory/skus/${skuId}/warehouses/${warehouseId}/lead-time`, payload);
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
getInventoryBySku,
|
|
146
|
+
getInventoryBySkuBatch,
|
|
147
|
+
getInventoryByWarehouse,
|
|
148
|
+
updateInventoryLeadTime,
|
|
149
|
+
updateInventoryQuantity
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=vtex-logistics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/vtex/vtex-logistics.ts"],
|
|
4
|
+
"sourcesContent": ["import axios from \"axios\";\nimport { vtexApi } from \"./vtex-api.js\";\n\nexport interface WarehouseBalance {\n warehouseId?: string;\n warehouseName?: string;\n totalQuantity?: number;\n reservedQuantity?: number;\n availableQuantity?: number;\n isUnlimited?: boolean;\n hasUnlimitedQuantity?: boolean;\n timeToRefill?: string;\n dateOfSupplyUtc?: string;\n leadTime?: string;\n}\n\nexport interface InventoryBySkuResponse {\n skuId?: string;\n balance?: WarehouseBalance[];\n}\n\nexport interface InventoryByWarehouseItem {\n skuId?: string;\n warehouseId?: string;\n totalQuantity?: number;\n reservedQuantity?: number;\n availableQuantity?: number;\n isUnlimited?: boolean;\n keepSellingAfterExpiration?: boolean;\n}\n\nexport interface UpdateInventoryQuantityPayload {\n quantity: number;\n unlimitedQuantity: boolean;\n dateUtcOnBalanceSystem?: string;\n}\n\nexport interface UpdateLeadTimePayload {\n leadTime: string;\n}\n\nexport interface GetInventoryBatchOptions {\n maxConcurrency?: number;\n maxRetries?: number;\n baseRetryDelayMs?: number;\n}\n\nexport interface InventoryBatchSuccess {\n skuId: string;\n document: InventoryBySkuResponse;\n}\n\nexport interface InventoryBatchFailure {\n skuId: string;\n message: string;\n statusCode?: number;\n attempts: number;\n retryable: boolean;\n}\n\nexport interface InventoryBatchResult {\n successful: InventoryBatchSuccess[];\n failed: InventoryBatchFailure[];\n}\n\ninterface InventoryFetchError extends Error {\n skuId: string;\n statusCode?: number;\n attempts: number;\n retryable: boolean;\n}\n\nconst RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);\nconst DEFAULT_BATCH_CONCURRENCY = 10;\nconst DEFAULT_MAX_RETRIES = 2;\nconst DEFAULT_BASE_RETRY_DELAY_MS = 250;\n\nexport async function getInventoryBySku(skuId: string): Promise<InventoryBySkuResponse> {\n const response = await vtexApi.get<InventoryBySkuResponse>(`/api/logistics/pvt/inventory/skus/${skuId}`);\n return response.data;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nfunction parseRetryAfterMs(headerValue: unknown): number | undefined {\n if (Array.isArray(headerValue)) {\n return parseRetryAfterMs(headerValue[0]);\n }\n\n if (typeof headerValue === \"number\" && Number.isFinite(headerValue)) {\n return Math.max(0, Math.floor(headerValue * 1000));\n }\n\n if (typeof headerValue !== \"string\") {\n return undefined;\n }\n\n const trimmedValue = headerValue.trim();\n if (!trimmedValue) {\n return undefined;\n }\n\n const seconds = Number(trimmedValue);\n if (Number.isFinite(seconds)) {\n return Math.max(0, Math.floor(seconds * 1000));\n }\n\n const absoluteDateMs = Date.parse(trimmedValue);\n if (Number.isNaN(absoluteDateMs)) {\n return undefined;\n }\n\n return Math.max(0, absoluteDateMs - Date.now());\n}\n\nfunction computeBackoffDelayMs(attempt: number, baseDelayMs: number): number {\n const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);\n const jitter = Math.floor(Math.random() * 150);\n return exponential + jitter;\n}\n\nfunction isRetryableRequestError(error: unknown): boolean {\n if (!axios.isAxiosError(error)) {\n return false;\n }\n\n if (!error.response?.status) {\n return true;\n }\n\n return RETRYABLE_STATUS_CODES.has(error.response.status);\n}\n\nfunction toInventoryFetchError(\n skuId: string,\n source: unknown,\n attempts: number,\n retryable: boolean\n): InventoryFetchError {\n const statusCode = axios.isAxiosError(source) ? source.response?.status : undefined;\n const message =\n source instanceof Error\n ? source.message\n : typeof source === \"string\"\n ? source\n : \"Unexpected error while fetching inventory details\";\n\n const error = new Error(message) as InventoryFetchError;\n error.skuId = skuId;\n error.statusCode = statusCode;\n error.attempts = attempts;\n error.retryable = retryable;\n return error;\n}\n\nfunction isInventoryFetchError(error: unknown): error is InventoryFetchError {\n if (!(error instanceof Error)) {\n return false;\n }\n\n return (\n \"skuId\" in error &&\n \"attempts\" in error &&\n \"retryable\" in error &&\n typeof (error as Partial<InventoryFetchError>).skuId === \"string\"\n );\n}\n\nasync function getInventoryBySkuWithRetry(\n skuId: string,\n options: GetInventoryBatchOptions\n): Promise<InventoryBatchSuccess> {\n const maxRetries = Math.max(0, Math.floor(options.maxRetries ?? DEFAULT_MAX_RETRIES));\n const baseRetryDelayMs = Math.max(\n 50,\n Math.floor(options.baseRetryDelayMs ?? DEFAULT_BASE_RETRY_DELAY_MS)\n );\n let attempts = 0;\n\n while (true) {\n attempts += 1;\n\n try {\n const document = await getInventoryBySku(skuId);\n return { skuId, document };\n } catch (error) {\n const retryable = isRetryableRequestError(error);\n\n if (!retryable || attempts > maxRetries) {\n throw toInventoryFetchError(skuId, error, attempts, retryable);\n }\n\n const retryAfterHeader = axios.isAxiosError(error)\n ? error.response?.headers?.[\"retry-after\"]\n : undefined;\n const retryAfterMs = parseRetryAfterMs(retryAfterHeader);\n const delayMs = retryAfterMs ?? computeBackoffDelayMs(attempts, baseRetryDelayMs);\n await sleep(delayMs);\n }\n }\n}\n\nexport async function getInventoryBySkuBatch(\n skuIds: string[],\n options: GetInventoryBatchOptions = {}\n): Promise<InventoryBatchResult> {\n const maxConcurrency = Math.max(\n 1,\n Math.floor(options.maxConcurrency ?? DEFAULT_BATCH_CONCURRENCY)\n );\n const successful: InventoryBatchSuccess[] = [];\n const failed: InventoryBatchFailure[] = [];\n\n for (let start = 0; start < skuIds.length; start += maxConcurrency) {\n const chunk = skuIds.slice(start, start + maxConcurrency);\n const settledChunk = await Promise.allSettled(\n chunk.map((skuId) => getInventoryBySkuWithRetry(skuId, options))\n );\n\n settledChunk.forEach((result, index) => {\n const skuId = chunk[index];\n\n if (result.status === \"fulfilled\") {\n successful.push(result.value);\n return;\n }\n\n if (isInventoryFetchError(result.reason)) {\n failed.push({\n skuId: result.reason.skuId,\n message: result.reason.message,\n statusCode: result.reason.statusCode,\n attempts: result.reason.attempts,\n retryable: result.reason.retryable,\n });\n return;\n }\n\n failed.push({\n skuId,\n message:\n result.reason instanceof Error\n ? result.reason.message\n : \"Unexpected error while fetching inventory details\",\n attempts: 1,\n retryable: false,\n });\n });\n }\n\n return { successful, failed };\n}\n\nexport async function getInventoryByWarehouse(\n skuId: string,\n warehouseId: string\n): Promise<InventoryByWarehouseItem[]> {\n const response = await vtexApi.get<InventoryByWarehouseItem[]>(\n `/api/logistics/pvt/inventory/items/${skuId}/warehouses/${warehouseId}`\n );\n return response.data;\n}\n\nexport async function updateInventoryQuantity(\n skuId: string,\n warehouseId: string,\n payload: UpdateInventoryQuantityPayload\n): Promise<void> {\n await vtexApi.patch(`/api/logistics/pvt/inventory/skus/${skuId}/warehouses/${warehouseId}/quantity`, payload);\n}\n\nexport async function updateInventoryLeadTime(\n skuId: string,\n warehouseId: string,\n payload: UpdateLeadTimePayload\n): Promise<void> {\n await vtexApi.patch(`/api/logistics/pvt/inventory/skus/${skuId}/warehouses/${warehouseId}/lead-time`, payload);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { vtexApi } from "./vtex-api.js";
|
|
3
|
+
const RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
4
|
+
const DEFAULT_BATCH_CONCURRENCY = 10;
|
|
5
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
6
|
+
const DEFAULT_BASE_RETRY_DELAY_MS = 250;
|
|
7
|
+
async function listOrders(params) {
|
|
8
|
+
const response = await vtexApi.get("/api/oms/pvt/orders", { params });
|
|
9
|
+
return response.data;
|
|
10
|
+
}
|
|
11
|
+
async function getOrderDocument(orderId, reason) {
|
|
12
|
+
const response = await vtexApi.get(`/api/orders/pvt/document/${orderId}`, {
|
|
13
|
+
params: reason ? { reason } : void 0
|
|
14
|
+
});
|
|
15
|
+
return response.data;
|
|
16
|
+
}
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
setTimeout(resolve, ms);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function parseRetryAfterMs(headerValue) {
|
|
23
|
+
if (Array.isArray(headerValue)) {
|
|
24
|
+
return parseRetryAfterMs(headerValue[0]);
|
|
25
|
+
}
|
|
26
|
+
if (typeof headerValue === "number" && Number.isFinite(headerValue)) {
|
|
27
|
+
return Math.max(0, Math.floor(headerValue * 1e3));
|
|
28
|
+
}
|
|
29
|
+
if (typeof headerValue !== "string") {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const trimmedValue = headerValue.trim();
|
|
33
|
+
if (!trimmedValue) {
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
const seconds = Number(trimmedValue);
|
|
37
|
+
if (Number.isFinite(seconds)) {
|
|
38
|
+
return Math.max(0, Math.floor(seconds * 1e3));
|
|
39
|
+
}
|
|
40
|
+
const absoluteDateMs = Date.parse(trimmedValue);
|
|
41
|
+
if (Number.isNaN(absoluteDateMs)) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
return Math.max(0, absoluteDateMs - Date.now());
|
|
45
|
+
}
|
|
46
|
+
function computeBackoffDelayMs(attempt, baseDelayMs) {
|
|
47
|
+
const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
48
|
+
const jitter = Math.floor(Math.random() * 150);
|
|
49
|
+
return exponential + jitter;
|
|
50
|
+
}
|
|
51
|
+
function isRetryableRequestError(error) {
|
|
52
|
+
if (!axios.isAxiosError(error)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!error.response?.status) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return RETRYABLE_STATUS_CODES.has(error.response.status);
|
|
59
|
+
}
|
|
60
|
+
function toOrderDocumentFetchError(orderId, source, attempts, retryable) {
|
|
61
|
+
const statusCode = axios.isAxiosError(source) ? source.response?.status : void 0;
|
|
62
|
+
const message = source instanceof Error ? source.message : typeof source === "string" ? source : "Unexpected error while fetching order details";
|
|
63
|
+
const error = new Error(message);
|
|
64
|
+
error.orderId = orderId;
|
|
65
|
+
error.statusCode = statusCode;
|
|
66
|
+
error.attempts = attempts;
|
|
67
|
+
error.retryable = retryable;
|
|
68
|
+
return error;
|
|
69
|
+
}
|
|
70
|
+
function isOrderDocumentFetchError(error) {
|
|
71
|
+
if (!(error instanceof Error)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return "orderId" in error && "attempts" in error && "retryable" in error && typeof error.orderId === "string";
|
|
75
|
+
}
|
|
76
|
+
async function getOrderDocumentWithRetry(orderId, options) {
|
|
77
|
+
const maxRetries = Math.max(0, Math.floor(options.maxRetries ?? DEFAULT_MAX_RETRIES));
|
|
78
|
+
const baseRetryDelayMs = Math.max(
|
|
79
|
+
50,
|
|
80
|
+
Math.floor(options.baseRetryDelayMs ?? DEFAULT_BASE_RETRY_DELAY_MS)
|
|
81
|
+
);
|
|
82
|
+
let attempts = 0;
|
|
83
|
+
while (true) {
|
|
84
|
+
attempts += 1;
|
|
85
|
+
try {
|
|
86
|
+
const document = await getOrderDocument(orderId, options.reason);
|
|
87
|
+
return { orderId, document };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const retryable = isRetryableRequestError(error);
|
|
90
|
+
if (!retryable || attempts > maxRetries) {
|
|
91
|
+
throw toOrderDocumentFetchError(orderId, error, attempts, retryable);
|
|
92
|
+
}
|
|
93
|
+
const retryAfterHeader = axios.isAxiosError(error) ? error.response?.headers?.["retry-after"] : void 0;
|
|
94
|
+
const retryAfterMs = parseRetryAfterMs(retryAfterHeader);
|
|
95
|
+
const delayMs = retryAfterMs ?? computeBackoffDelayMs(attempts, baseRetryDelayMs);
|
|
96
|
+
await sleep(delayMs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function getOrderDocumentsBatch(orderIds, options = {}) {
|
|
101
|
+
const maxConcurrency = Math.max(
|
|
102
|
+
1,
|
|
103
|
+
Math.floor(options.maxConcurrency ?? DEFAULT_BATCH_CONCURRENCY)
|
|
104
|
+
);
|
|
105
|
+
const successful = [];
|
|
106
|
+
const failed = [];
|
|
107
|
+
for (let start = 0; start < orderIds.length; start += maxConcurrency) {
|
|
108
|
+
const chunk = orderIds.slice(start, start + maxConcurrency);
|
|
109
|
+
const settledChunk = await Promise.allSettled(
|
|
110
|
+
chunk.map((orderId) => getOrderDocumentWithRetry(orderId, options))
|
|
111
|
+
);
|
|
112
|
+
settledChunk.forEach((result, index) => {
|
|
113
|
+
const orderId = chunk[index];
|
|
114
|
+
if (result.status === "fulfilled") {
|
|
115
|
+
successful.push(result.value);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (isOrderDocumentFetchError(result.reason)) {
|
|
119
|
+
failed.push({
|
|
120
|
+
orderId: result.reason.orderId,
|
|
121
|
+
message: result.reason.message,
|
|
122
|
+
statusCode: result.reason.statusCode,
|
|
123
|
+
attempts: result.reason.attempts,
|
|
124
|
+
retryable: result.reason.retryable
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
failed.push({
|
|
129
|
+
orderId,
|
|
130
|
+
message: result.reason instanceof Error ? result.reason.message : "Unexpected error while fetching order details",
|
|
131
|
+
attempts: 1,
|
|
132
|
+
retryable: false
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return { successful, failed };
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
getOrderDocument,
|
|
140
|
+
getOrderDocumentsBatch,
|
|
141
|
+
listOrders
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=vtex-orders.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/vtex/vtex-orders.ts"],
|
|
4
|
+
"sourcesContent": ["import axios from \"axios\";\nimport { vtexApi } from \"./vtex-api.js\";\n\nexport interface VtexOrderListItem {\n orderId: string;\n creationDate?: string;\n status?: string;\n totalValue?: number;\n currencyCode?: string;\n paymentNames?: string;\n salesChannel?: string;\n origin?: string;\n totalItems?: number;\n lastChange?: string;\n orderIsComplete?: boolean;\n authorizedDate?: string;\n paymentApprovedDate?: string;\n readyForHandlingDate?: string;\n}\n\nexport interface VtexOrdersPaging {\n total?: number;\n pages?: number;\n currentPage?: number;\n perPage?: number;\n}\n\nexport interface VtexOrdersListResponse {\n list: VtexOrderListItem[];\n paging: VtexOrdersPaging;\n stats?: unknown;\n}\n\nexport interface ListOrdersParams {\n page?: number;\n per_page?: number;\n orderBy?: string;\n f_creationDate?: string;\n f_invoicedDate?: string;\n f_status?: string;\n f_salesChannel?: string;\n f_paymentNames?: string;\n f_UtmSource?: string;\n q?: string;\n}\n\nexport interface GetOrderDocumentsBatchOptions {\n reason?: string;\n maxConcurrency?: number;\n maxRetries?: number;\n baseRetryDelayMs?: number;\n}\n\nexport interface OrderDocumentSuccess {\n orderId: string;\n document: Record<string, unknown>;\n}\n\nexport interface OrderDocumentFailure {\n orderId: string;\n message: string;\n statusCode?: number;\n attempts: number;\n retryable: boolean;\n}\n\nexport interface OrderDocumentsBatchResult {\n successful: OrderDocumentSuccess[];\n failed: OrderDocumentFailure[];\n}\n\nconst RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);\nconst DEFAULT_BATCH_CONCURRENCY = 10;\nconst DEFAULT_MAX_RETRIES = 2;\nconst DEFAULT_BASE_RETRY_DELAY_MS = 250;\n\nexport async function listOrders(params: ListOrdersParams): Promise<VtexOrdersListResponse> {\n const response = await vtexApi.get<VtexOrdersListResponse>(\"/api/oms/pvt/orders\", { params });\n return response.data;\n}\n\nexport async function getOrderDocument(\n orderId: string,\n reason?: string\n): Promise<Record<string, unknown>> {\n const response = await vtexApi.get<Record<string, unknown>>(`/api/orders/pvt/document/${orderId}`, {\n params: reason ? { reason } : undefined,\n });\n\n return response.data;\n}\n\ninterface OrderDocumentFetchError extends Error {\n orderId: string;\n statusCode?: number;\n attempts: number;\n retryable: boolean;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nfunction parseRetryAfterMs(headerValue: unknown): number | undefined {\n if (Array.isArray(headerValue)) {\n return parseRetryAfterMs(headerValue[0]);\n }\n\n if (typeof headerValue === \"number\" && Number.isFinite(headerValue)) {\n return Math.max(0, Math.floor(headerValue * 1000));\n }\n\n if (typeof headerValue !== \"string\") {\n return undefined;\n }\n\n const trimmedValue = headerValue.trim();\n if (!trimmedValue) {\n return undefined;\n }\n\n const seconds = Number(trimmedValue);\n if (Number.isFinite(seconds)) {\n return Math.max(0, Math.floor(seconds * 1000));\n }\n\n const absoluteDateMs = Date.parse(trimmedValue);\n if (Number.isNaN(absoluteDateMs)) {\n return undefined;\n }\n\n return Math.max(0, absoluteDateMs - Date.now());\n}\n\nfunction computeBackoffDelayMs(attempt: number, baseDelayMs: number): number {\n const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);\n const jitter = Math.floor(Math.random() * 150);\n return exponential + jitter;\n}\n\nfunction isRetryableRequestError(error: unknown): boolean {\n if (!axios.isAxiosError(error)) {\n return false;\n }\n\n if (!error.response?.status) {\n return true;\n }\n\n return RETRYABLE_STATUS_CODES.has(error.response.status);\n}\n\nfunction toOrderDocumentFetchError(\n orderId: string,\n source: unknown,\n attempts: number,\n retryable: boolean\n): OrderDocumentFetchError {\n const statusCode = axios.isAxiosError(source) ? source.response?.status : undefined;\n const message =\n source instanceof Error\n ? source.message\n : typeof source === \"string\"\n ? source\n : \"Unexpected error while fetching order details\";\n\n const error = new Error(message) as OrderDocumentFetchError;\n error.orderId = orderId;\n error.statusCode = statusCode;\n error.attempts = attempts;\n error.retryable = retryable;\n return error;\n}\n\nfunction isOrderDocumentFetchError(error: unknown): error is OrderDocumentFetchError {\n if (!(error instanceof Error)) {\n return false;\n }\n\n return (\n \"orderId\" in error &&\n \"attempts\" in error &&\n \"retryable\" in error &&\n typeof (error as Partial<OrderDocumentFetchError>).orderId === \"string\"\n );\n}\n\nasync function getOrderDocumentWithRetry(\n orderId: string,\n options: GetOrderDocumentsBatchOptions\n): Promise<OrderDocumentSuccess> {\n const maxRetries = Math.max(0, Math.floor(options.maxRetries ?? DEFAULT_MAX_RETRIES));\n const baseRetryDelayMs = Math.max(\n 50,\n Math.floor(options.baseRetryDelayMs ?? DEFAULT_BASE_RETRY_DELAY_MS)\n );\n let attempts = 0;\n\n while (true) {\n attempts += 1;\n\n try {\n const document = await getOrderDocument(orderId, options.reason);\n return { orderId, document };\n } catch (error) {\n const retryable = isRetryableRequestError(error);\n\n if (!retryable || attempts > maxRetries) {\n throw toOrderDocumentFetchError(orderId, error, attempts, retryable);\n }\n\n const retryAfterHeader = axios.isAxiosError(error)\n ? error.response?.headers?.[\"retry-after\"]\n : undefined;\n const retryAfterMs = parseRetryAfterMs(retryAfterHeader);\n const delayMs = retryAfterMs ?? computeBackoffDelayMs(attempts, baseRetryDelayMs);\n await sleep(delayMs);\n }\n }\n}\n\nexport async function getOrderDocumentsBatch(\n orderIds: string[],\n options: GetOrderDocumentsBatchOptions = {}\n): Promise<OrderDocumentsBatchResult> {\n const maxConcurrency = Math.max(\n 1,\n Math.floor(options.maxConcurrency ?? DEFAULT_BATCH_CONCURRENCY)\n );\n const successful: OrderDocumentSuccess[] = [];\n const failed: OrderDocumentFailure[] = [];\n\n for (let start = 0; start < orderIds.length; start += maxConcurrency) {\n const chunk = orderIds.slice(start, start + maxConcurrency);\n const settledChunk = await Promise.allSettled(\n chunk.map((orderId) => getOrderDocumentWithRetry(orderId, options))\n );\n\n settledChunk.forEach((result, index) => {\n const orderId = chunk[index];\n\n if (result.status === \"fulfilled\") {\n successful.push(result.value);\n return;\n }\n\n if (isOrderDocumentFetchError(result.reason)) {\n failed.push({\n orderId: result.reason.orderId,\n message: result.reason.message,\n statusCode: result.reason.statusCode,\n attempts: result.reason.attempts,\n retryable: result.reason.retryable,\n });\n return;\n }\n\n failed.push({\n orderId,\n message:\n result.reason instanceof Error\n ? result.reason.message\n : \"Unexpected error while fetching order details\",\n attempts: 1,\n retryable: false,\n });\n });\n }\n\n return { successful, failed };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { vtexPricingApi } from "./vtex-api.js";
|
|
2
|
+
async function getSkuPrice(itemId) {
|
|
3
|
+
const response = await vtexPricingApi.get(`/pricing/prices/${itemId}`);
|
|
4
|
+
return response.data;
|
|
5
|
+
}
|
|
6
|
+
async function getComputedPrice(itemId, priceTableId, params) {
|
|
7
|
+
const response = await vtexPricingApi.get(
|
|
8
|
+
`/pricing/prices/${itemId}/computed/${priceTableId}`,
|
|
9
|
+
{ params }
|
|
10
|
+
);
|
|
11
|
+
return response.data;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
getComputedPrice,
|
|
15
|
+
getSkuPrice
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=vtex-pricing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/services/vtex/vtex-pricing.ts"],
|
|
4
|
+
"sourcesContent": ["import { vtexPricingApi } from \"./vtex-api.js\";\n\nexport interface SkuFixedPrice {\n value?: number;\n listPrice?: number;\n minQuantity?: number;\n dateRange?: {\n from?: string;\n to?: string;\n };\n tradePolicyId?: string;\n}\n\nexport interface SkuPriceResponse {\n itemId: string;\n listPrice: number;\n costPrice: number;\n markup: number;\n basePrice: number;\n fixedPrices: SkuFixedPrice[];\n}\n\nexport interface ComputedPriceParams {\n categoryId: number;\n brandId: number;\n quantity: number;\n}\n\nexport interface ComputedPriceResponse {\n tradePolicyId: string;\n listPrice: number;\n costPrice?: number;\n sellingPrice: number;\n priceValidUntil: string;\n}\n\nexport async function getSkuPrice(itemId: string): Promise<SkuPriceResponse> {\n const response = await vtexPricingApi.get<SkuPriceResponse>(`/pricing/prices/${itemId}`);\n return response.data;\n}\n\nexport async function getComputedPrice(\n itemId: string,\n priceTableId: string,\n params: ComputedPriceParams\n): Promise<ComputedPriceResponse> {\n const response = await vtexPricingApi.get<ComputedPriceResponse>(\n `/pricing/prices/${itemId}/computed/${priceTableId}`,\n { params }\n );\n\n return response.data;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,sBAAsB;AAoC/B,eAAsB,YAAY,QAA2C;AAC3E,QAAM,WAAW,MAAM,eAAe,IAAsB,mBAAmB,MAAM,EAAE;AACvF,SAAO,SAAS;AAClB;AAEA,eAAsB,iBACpB,QACA,cACA,QACgC;AAChC,QAAM,WAAW,MAAM,eAAe;AAAA,IACpC,mBAAmB,MAAM,aAAa,YAAY;AAAA,IAClD,EAAE,OAAO;AAAA,EACX;AAEA,SAAO,SAAS;AAClB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { error, object } from "mcp-use/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
detectSeverity,
|
|
5
|
+
extractQuotaSnapshot,
|
|
6
|
+
getDimensionValue,
|
|
7
|
+
getMetricValueByName,
|
|
8
|
+
isProblematicAttributionValue,
|
|
9
|
+
resolveGa4PropertyId,
|
|
10
|
+
round,
|
|
11
|
+
toPercent
|
|
12
|
+
} from "../../analytics/ga4-report-utils.js";
|
|
13
|
+
import { runReport } from "../../services/analytics/ga4-client.js";
|
|
14
|
+
import { stripNulls } from "../../utils/strip-payload.js";
|
|
15
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
16
|
+
const attributionGapsSchema = z.object({
|
|
17
|
+
startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
|
|
18
|
+
endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
|
|
19
|
+
propertyId: z.string().optional().describe("GA4 property ID. If omitted, uses GA4_PROPERTY_ID"),
|
|
20
|
+
limit: z.number().int().min(10).max(100).optional().describe("Maximum source / medium rows to inspect")
|
|
21
|
+
});
|
|
22
|
+
async function attributionGapsHandler(params) {
|
|
23
|
+
try {
|
|
24
|
+
const propertyId = resolveGa4PropertyId(params.propertyId);
|
|
25
|
+
const limit = params.limit ?? 50;
|
|
26
|
+
const [channelReport, sourceMediumReport, totalReport] = await Promise.all([
|
|
27
|
+
runReport(propertyId, {
|
|
28
|
+
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
29
|
+
dimensions: [{ name: "sessionDefaultChannelGroup" }],
|
|
30
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }]
|
|
31
|
+
}),
|
|
32
|
+
runReport(propertyId, {
|
|
33
|
+
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
34
|
+
dimensions: [{ name: "sessionSourceMedium" }],
|
|
35
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
36
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
37
|
+
limit,
|
|
38
|
+
returnPropertyQuota: true
|
|
39
|
+
}),
|
|
40
|
+
runReport(propertyId, {
|
|
41
|
+
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
42
|
+
metrics: [{ name: "sessions" }]
|
|
43
|
+
})
|
|
44
|
+
]);
|
|
45
|
+
const totalSessions = getMetricValueByName(
|
|
46
|
+
totalReport.rows?.[0] ?? {},
|
|
47
|
+
totalReport.metricHeaders,
|
|
48
|
+
"sessions"
|
|
49
|
+
);
|
|
50
|
+
const unassignedChannel = (channelReport.rows ?? []).find(
|
|
51
|
+
(row) => getDimensionValue(row) === "Unassigned"
|
|
52
|
+
);
|
|
53
|
+
const unassignedSessions = getMetricValueByName(
|
|
54
|
+
unassignedChannel ?? {},
|
|
55
|
+
channelReport.metricHeaders,
|
|
56
|
+
"sessions"
|
|
57
|
+
);
|
|
58
|
+
const problematicSourceMediumRows = (sourceMediumReport.rows ?? []).filter((row) => isProblematicAttributionValue(getDimensionValue(row))).map((row) => {
|
|
59
|
+
const sessions = getMetricValueByName(row, sourceMediumReport.metricHeaders, "sessions");
|
|
60
|
+
const users = getMetricValueByName(row, sourceMediumReport.metricHeaders, "totalUsers");
|
|
61
|
+
return {
|
|
62
|
+
source_medium: getDimensionValue(row),
|
|
63
|
+
sessions,
|
|
64
|
+
users,
|
|
65
|
+
session_share_percent: round(toPercent(sessions, totalSessions))
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const problematicSourceMediumSessions = problematicSourceMediumRows.reduce(
|
|
69
|
+
(total, row) => total + row.sessions,
|
|
70
|
+
0
|
|
71
|
+
);
|
|
72
|
+
const unassignedSharePercent = round(toPercent(unassignedSessions, totalSessions));
|
|
73
|
+
const problematicSourceMediumSharePercent = round(
|
|
74
|
+
toPercent(problematicSourceMediumSessions, totalSessions)
|
|
75
|
+
);
|
|
76
|
+
return object(
|
|
77
|
+
stripNulls({
|
|
78
|
+
property_id: propertyId,
|
|
79
|
+
date_range: {
|
|
80
|
+
start_date: params.startDate,
|
|
81
|
+
end_date: params.endDate
|
|
82
|
+
},
|
|
83
|
+
overview: {
|
|
84
|
+
total_sessions: totalSessions,
|
|
85
|
+
attribution_warning: "Las se\xF1ales se reportan por separado para evitar doble conteo entre dimensiones distintas de GA4."
|
|
86
|
+
},
|
|
87
|
+
unassigned_channel_group: {
|
|
88
|
+
sessions: unassignedSessions,
|
|
89
|
+
session_share_percent: unassignedSharePercent,
|
|
90
|
+
severity: detectSeverity(unassignedSharePercent)
|
|
91
|
+
},
|
|
92
|
+
problematic_source_medium: {
|
|
93
|
+
sessions: problematicSourceMediumSessions,
|
|
94
|
+
session_share_percent: problematicSourceMediumSharePercent,
|
|
95
|
+
severity: detectSeverity(problematicSourceMediumSharePercent)
|
|
96
|
+
},
|
|
97
|
+
problematic_source_mediums: problematicSourceMediumRows,
|
|
98
|
+
quota: extractQuotaSnapshot(sourceMediumReport)
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return error(err instanceof Error ? err.message : "Failed to detect GA4 attribution gaps");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
attributionGapsHandler,
|
|
107
|
+
attributionGapsSchema
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=attribution-gaps.js.map
|