@yoryoboy/bi-mcp 1.5.0 → 1.5.2
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 +1 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/mcp-use.json +2 -2
- package/dist/src/analytics/ga4-report-utils.js +20 -47
- package/dist/src/analytics/ga4-report-utils.js.map +2 -2
- package/dist/src/config/google-store.js +96 -0
- package/dist/src/config/google-store.js.map +7 -0
- package/dist/src/config/google.js +4 -15
- package/dist/src/config/google.js.map +2 -2
- package/dist/src/services/analytics/oauth.js +20 -7
- package/dist/src/services/analytics/oauth.js.map +2 -2
- package/dist/src/services/search-console/search-console-utils.js +49 -170
- package/dist/src/services/search-console/search-console-utils.js.map +2 -2
- package/dist/src/tools/analytics/attribution-gaps.js +3 -2
- package/dist/src/tools/analytics/attribution-gaps.js.map +2 -2
- package/dist/src/tools/analytics/channel-mix.js +3 -2
- package/dist/src/tools/analytics/channel-mix.js.map +2 -2
- package/dist/src/tools/analytics/ecommerce-tracking-health.js +3 -2
- package/dist/src/tools/analytics/ecommerce-tracking-health.js.map +2 -2
- package/dist/src/tools/analytics/engagement-overview.js +3 -2
- package/dist/src/tools/analytics/engagement-overview.js.map +2 -2
- package/dist/src/tools/analytics/property-info.js +3 -2
- package/dist/src/tools/analytics/property-info.js.map +2 -2
- package/dist/src/tools/analytics/revenue-by-channel.js +3 -2
- package/dist/src/tools/analytics/revenue-by-channel.js.map +2 -2
- package/dist/src/tools/analytics/revenue-overview.js +3 -2
- package/dist/src/tools/analytics/revenue-overview.js.map +2 -2
- package/dist/src/tools/analytics/revenue-trend.js +3 -2
- package/dist/src/tools/analytics/revenue-trend.js.map +2 -2
- package/dist/src/tools/analytics/source-medium-breakdown.js +3 -2
- package/dist/src/tools/analytics/source-medium-breakdown.js.map +2 -2
- package/dist/src/tools/analytics/top-landing-pages.js +3 -2
- package/dist/src/tools/analytics/top-landing-pages.js.map +2 -2
- package/dist/src/tools/config/list-profiles.js +23 -8
- package/dist/src/tools/config/list-profiles.js.map +2 -2
- package/dist/src/tools/google-ads/account-overview.js +3 -2
- package/dist/src/tools/google-ads/account-overview.js.map +2 -2
- package/dist/src/tools/google-ads/account-risks.js +3 -2
- package/dist/src/tools/google-ads/account-risks.js.map +2 -2
- package/dist/src/tools/google-ads/break-even-analysis.js +3 -2
- package/dist/src/tools/google-ads/break-even-analysis.js.map +2 -2
- package/dist/src/tools/google-ads/campaign-performance.js +3 -2
- package/dist/src/tools/google-ads/campaign-performance.js.map +2 -2
- package/dist/src/tools/google-ads/channel-mix.js +3 -2
- package/dist/src/tools/google-ads/channel-mix.js.map +2 -2
- package/dist/src/tools/google-ads/compare-accounts.js +1 -1
- package/dist/src/tools/google-ads/compare-accounts.js.map +2 -2
- package/dist/src/tools/google-ads/customer-clients.js +3 -2
- package/dist/src/tools/google-ads/customer-clients.js.map +2 -2
- package/dist/src/tools/google-ads/customer-info.js +3 -2
- package/dist/src/tools/google-ads/customer-info.js.map +2 -2
- package/dist/src/tools/google-ads/scaling-health.js +3 -2
- package/dist/src/tools/google-ads/scaling-health.js.map +2 -2
- package/dist/src/tools/google-ads/search-terms-summary.js +3 -2
- package/dist/src/tools/google-ads/search-terms-summary.js.map +2 -2
- package/dist/src/tools/google-ads/time-series.js +3 -2
- package/dist/src/tools/google-ads/time-series.js.map +2 -2
- package/dist/src/tools/search-console/country-breakdown.js +1 -1
- package/dist/src/tools/search-console/country-breakdown.js.map +2 -2
- package/dist/src/tools/search-console/device-breakdown.js +1 -1
- package/dist/src/tools/search-console/device-breakdown.js.map +2 -2
- package/dist/src/tools/search-console/high-impression-low-click-queries.js +1 -1
- package/dist/src/tools/search-console/high-impression-low-click-queries.js.map +2 -2
- package/dist/src/tools/search-console/low-ctr-opportunities.js +1 -1
- package/dist/src/tools/search-console/low-ctr-opportunities.js.map +2 -2
- package/dist/src/tools/search-console/page-performance.js +1 -1
- package/dist/src/tools/search-console/page-performance.js.map +2 -2
- package/dist/src/tools/search-console/product-demand-low-capture-queries.js +1 -1
- package/dist/src/tools/search-console/product-demand-low-capture-queries.js.map +2 -2
- package/dist/src/tools/search-console/query-page-matrix.js +1 -1
- package/dist/src/tools/search-console/query-page-matrix.js.map +2 -2
- package/dist/src/tools/search-console/query-performance.js +1 -1
- package/dist/src/tools/search-console/query-performance.js.map +2 -2
- package/dist/src/tools/search-console/quick-win-opportunities.js +1 -1
- package/dist/src/tools/search-console/quick-win-opportunities.js.map +2 -2
- package/dist/src/tools/search-console/rising-non-brand-queries.js +1 -1
- package/dist/src/tools/search-console/rising-non-brand-queries.js.map +2 -2
- package/dist/src/tools/search-console/search-performance.js +1 -1
- package/dist/src/tools/search-console/search-performance.js.map +2 -2
- package/dist/src/tools/search-console/site-context.js +2 -2
- package/dist/src/tools/search-console/site-context.js.map +2 -2
- package/dist/src/tools/search-console/visibility-declines.js +1 -1
- package/dist/src/tools/search-console/visibility-declines.js.map +2 -2
- package/dist/src/utils/google-ads.js +15 -26
- package/dist/src/utils/google-ads.js.map +2 -2
- package/package.json +1 -1
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { getProfileGoogleServiceMapping } from "../../config/google-store.js";
|
|
2
3
|
const searchConsoleDateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
4
|
const searchConsoleSearchTypes = ["web", "image", "video", "news", "discover"];
|
|
4
|
-
const searchConsoleDimensions = [
|
|
5
|
-
|
|
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
|
-
];
|
|
5
|
+
const searchConsoleDimensions = ["query", "page", "country", "device", "date", "searchAppearance"];
|
|
6
|
+
const searchConsoleFilterOperators = ["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"];
|
|
20
7
|
const searchConsoleAggregationTypes = ["auto", "byPage", "byProperty"];
|
|
21
|
-
const searchConsoleDimensionFilterSchema = z.object({
|
|
22
|
-
|
|
23
|
-
|
|
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.");
|
|
8
|
+
const searchConsoleDimensionFilterSchema = z.object({ dimension: z.enum(searchConsoleDimensions).describe("Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)"), operator: z.enum(searchConsoleFilterOperators).describe("Filter operator supported by Search Console."), expression: z.string().min(1).describe("Filter value or regex expression.") });
|
|
9
|
+
const siteUrlSchemaField = z.string().optional().describe("Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool tries profileId mapping first.");
|
|
10
|
+
const profileIdSchemaField = z.string().optional().describe("Optional business profile identifier used to resolve Google service mappings.");
|
|
30
11
|
const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Start date in YYYY-MM-DD format.");
|
|
31
12
|
const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("End date in YYYY-MM-DD format.");
|
|
32
13
|
const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period start date in YYYY-MM-DD format.");
|
|
@@ -37,49 +18,34 @@ const rowLimitSchemaField = z.number().int().min(1).max(25e3).optional().describ
|
|
|
37
18
|
const startRowSchemaField = z.number().int().min(0).optional().describe("Zero-based row offset for paginated Search Console requests.");
|
|
38
19
|
const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe("Search type to query. Defaults to web.");
|
|
39
20
|
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) {
|
|
21
|
+
async function resolveSearchConsoleSiteUrl(siteUrl, profileId) {
|
|
41
22
|
const explicitSiteUrl = siteUrl?.trim();
|
|
42
|
-
if (explicitSiteUrl)
|
|
43
|
-
|
|
23
|
+
if (explicitSiteUrl) return explicitSiteUrl;
|
|
24
|
+
const explicitProfileId = profileId?.trim();
|
|
25
|
+
if (explicitProfileId) {
|
|
26
|
+
const mapping = await getProfileGoogleServiceMapping(explicitProfileId);
|
|
27
|
+
if (mapping?.searchConsoleSiteUrl) return mapping.searchConsoleSiteUrl;
|
|
28
|
+
throw new Error(`Missing Search Console site URL. profileId "${explicitProfileId}" has no Search Console mapping configured.`);
|
|
44
29
|
}
|
|
45
|
-
|
|
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
|
-
);
|
|
30
|
+
throw new Error("Missing Search Console site URL. Pass siteUrl explicitly, provide profileId with a configured mapping, or call gsc_list_accessible_sites first.");
|
|
53
31
|
}
|
|
54
32
|
function inferSearchConsolePropertyType(siteUrl) {
|
|
55
|
-
if (siteUrl.startsWith("sc-domain:"))
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (/^https?:\/\//.test(siteUrl)) {
|
|
59
|
-
return "url_prefix";
|
|
60
|
-
}
|
|
33
|
+
if (siteUrl.startsWith("sc-domain:")) return "domain";
|
|
34
|
+
if (/^https?:\/\//.test(siteUrl)) return "url_prefix";
|
|
61
35
|
return "unknown";
|
|
62
36
|
}
|
|
63
37
|
function normalizePermissionLevel(permissionLevel) {
|
|
64
38
|
return permissionLevel?.trim().toLowerCase() || void 0;
|
|
65
39
|
}
|
|
66
40
|
function normalizeSearchConsoleSiteEntry(siteEntry) {
|
|
67
|
-
return {
|
|
68
|
-
site_url: siteEntry.siteUrl,
|
|
69
|
-
property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),
|
|
70
|
-
permission_level: normalizePermissionLevel(siteEntry.permissionLevel)
|
|
71
|
-
};
|
|
41
|
+
return { site_url: siteEntry.siteUrl, property_type: inferSearchConsolePropertyType(siteEntry.siteUrl), permission_level: normalizePermissionLevel(siteEntry.permissionLevel) };
|
|
72
42
|
}
|
|
73
43
|
function toPercent(numerator, denominator) {
|
|
74
|
-
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0)
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
44
|
+
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return 0;
|
|
77
45
|
return numerator / denominator * 100;
|
|
78
46
|
}
|
|
79
47
|
function round(value, decimals = 2) {
|
|
80
|
-
if (!Number.isFinite(value))
|
|
81
|
-
return 0;
|
|
82
|
-
}
|
|
48
|
+
if (!Number.isFinite(value)) return 0;
|
|
83
49
|
const factor = 10 ** decimals;
|
|
84
50
|
return Math.round(value * factor) / factor;
|
|
85
51
|
}
|
|
@@ -88,49 +54,21 @@ function normalizeSearchConsoleRow(row, dimensions = []) {
|
|
|
88
54
|
const impressions = row.impressions ?? 0;
|
|
89
55
|
const ctr = typeof row.ctr === "number" ? row.ctr * 100 : toPercent(clicks, impressions);
|
|
90
56
|
const keys = row.keys ?? [];
|
|
91
|
-
return {
|
|
92
|
-
keys
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
};
|
|
57
|
+
return { keys, dimensions: dimensions.reduce((accumulator, dimension, index) => {
|
|
58
|
+
accumulator[dimension] = keys[index] ?? "";
|
|
59
|
+
return accumulator;
|
|
60
|
+
}, {}), clicks: round(clicks, 2), impressions: round(impressions, 2), ctr_percent: round(ctr, 2), position: round(row.position ?? 0, 2) };
|
|
102
61
|
}
|
|
103
62
|
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
|
-
};
|
|
63
|
+
const dimensionFilterGroups = input.dimensionFilters && input.dimensionFilters.length > 0 ? [{ groupType: "and", filters: input.dimensionFilters }] : void 0;
|
|
64
|
+
return { startDate: input.startDate, endDate: input.endDate, dimensions: input.dimensions, type: input.searchType ?? "web", dimensionFilterGroups, aggregationType: input.aggregationType, rowLimit: input.rowLimit, startRow: input.startRow };
|
|
120
65
|
}
|
|
121
66
|
function buildPaginationMetadata(input) {
|
|
122
67
|
const startRow = input.startRow ?? 0;
|
|
123
68
|
const rowLimit = input.rowLimit;
|
|
124
69
|
const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;
|
|
125
70
|
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
|
-
};
|
|
71
|
+
return { is_partial: isPartial, start_row: startRow, row_limit: rowLimit, returned_rows: input.returnedRows, next_start_row: nextStartRow, 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 };
|
|
134
72
|
}
|
|
135
73
|
function parseBrandTerms(brandTerms) {
|
|
136
74
|
return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);
|
|
@@ -138,98 +76,41 @@ function parseBrandTerms(brandTerms) {
|
|
|
138
76
|
function isBrandedQuery(query, brandTerms) {
|
|
139
77
|
const normalizedQuery = query.trim().toLowerCase();
|
|
140
78
|
const normalizedBrandTerms = parseBrandTerms(brandTerms);
|
|
141
|
-
if (!normalizedQuery || normalizedBrandTerms.length === 0)
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
79
|
+
if (!normalizedQuery || normalizedBrandTerms.length === 0) return false;
|
|
144
80
|
return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));
|
|
145
81
|
}
|
|
146
82
|
function classifyBrandLabel(query, brandTerms) {
|
|
83
|
+
if ((brandTerms ?? []).length === 0) return "unknown";
|
|
147
84
|
return isBrandedQuery(query, brandTerms) ? "brand" : "non_brand";
|
|
148
85
|
}
|
|
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
86
|
function sumSearchConsoleRows(rows) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
};
|
|
87
|
+
const clicks = rows.reduce((total, row) => total + row.clicks, 0);
|
|
88
|
+
const impressions = rows.reduce((total, row) => total + row.impressions, 0);
|
|
89
|
+
const weightedCtr = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.ctr_percent, 0) / impressions : 0;
|
|
90
|
+
const weightedPosition = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.position, 0) / impressions : 0;
|
|
91
|
+
return { clicks: round(clicks), impressions: round(impressions), ctrPercent: round(weightedCtr), averagePosition: round(weightedPosition) };
|
|
181
92
|
}
|
|
182
|
-
function
|
|
183
|
-
return
|
|
93
|
+
function scoreQuickWinOpportunity(input) {
|
|
94
|
+
return round(input.impressions * Math.max(0, 8 - input.ctrPercent) * Math.max(0.5, 20 - input.position) / 100);
|
|
184
95
|
}
|
|
185
|
-
function
|
|
186
|
-
return
|
|
96
|
+
function scoreHighImpressionLowClickOpportunity(input) {
|
|
97
|
+
return round(input.impressions * Math.max(0, 5 - input.ctrPercent) * Math.max(0.5, 15 - input.position) / 100);
|
|
187
98
|
}
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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));
|
|
99
|
+
function classifyQuickWinType(position, ctrPercent) {
|
|
100
|
+
if (position > 0 && position <= 10 && ctrPercent < 4) return "ctr";
|
|
101
|
+
if (position > 10 && position <= 20) return "position";
|
|
102
|
+
return "mixed";
|
|
207
103
|
}
|
|
208
104
|
function looksLikeProductQuery(query, productTerms) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;
|
|
105
|
+
if ((productTerms ?? []).some((term) => query.toLowerCase().includes(term.trim().toLowerCase()))) return true;
|
|
106
|
+
return /\b(comprar|precio|modelo|sku|talle|color|review|opiniones)\b/i.test(query);
|
|
215
107
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
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);
|
|
108
|
+
function buildComparisonMap(rows, getKey) {
|
|
109
|
+
return new Map(rows.map((row) => [getKey(row), row]));
|
|
227
110
|
}
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
|
|
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);
|
|
111
|
+
function computeChangePercent(current, previous) {
|
|
112
|
+
if (previous === 0) return current > 0 ? 100 : 0;
|
|
113
|
+
return round((current - previous) / previous * 100);
|
|
233
114
|
}
|
|
234
115
|
export {
|
|
235
116
|
brandTermsSchemaField,
|
|
@@ -244,7 +125,6 @@ export {
|
|
|
244
125
|
endDateSchemaField,
|
|
245
126
|
inferSearchConsolePropertyType,
|
|
246
127
|
isBrandedQuery,
|
|
247
|
-
looksLikeCommercialQuery,
|
|
248
128
|
looksLikeProductQuery,
|
|
249
129
|
normalizePermissionLevel,
|
|
250
130
|
normalizeSearchConsoleRow,
|
|
@@ -269,7 +149,6 @@ export {
|
|
|
269
149
|
startDateSchemaField,
|
|
270
150
|
startRowSchemaField,
|
|
271
151
|
sumSearchConsoleRows,
|
|
272
|
-
toPercent
|
|
273
|
-
tokenizeQuery
|
|
152
|
+
toPercent
|
|
274
153
|
};
|
|
275
154
|
//# sourceMappingURL=search-console-utils.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/services/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 \"./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": "AAAA,SAAS,SAAS;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\nimport { getProfileGoogleServiceMapping } from '../../config/google-store.js';\nimport type { SearchConsoleDimensionFilter, SearchConsoleDimensionFilterGroup, SearchConsoleSearchAnalyticsRequest, SearchConsoleSearchAnalyticsRow, SearchConsoleSiteEntry } from './search-console-client.js';\n\nexport const searchConsoleDateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\nexport const searchConsoleSearchTypes = ['web', 'image', 'video', 'news', 'discover'] as const;\nexport const searchConsoleDimensions = ['query', 'page', 'country', 'device', 'date', 'searchAppearance'] as const;\nexport const searchConsoleFilterOperators = ['contains', 'equals', 'notContains', 'notEquals', 'includingRegex', 'excludingRegex'] as const;\nexport const searchConsoleAggregationTypes = ['auto', 'byPage', 'byProperty'] as const;\nexport const searchConsoleDimensionFilterSchema = z.object({ dimension: z.enum(searchConsoleDimensions).describe('Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)'), operator: z.enum(searchConsoleFilterOperators).describe('Filter operator supported by Search Console.'), expression: z.string().min(1).describe('Filter value or regex expression.') });\nexport const siteUrlSchemaField = z.string().optional().describe('Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool tries profileId mapping first.');\nexport const profileIdSchemaField = z.string().optional().describe('Optional business profile identifier used to resolve Google service mappings.');\nexport const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Start date in YYYY-MM-DD format.');\nexport const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('End date in YYYY-MM-DD format.');\nexport const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Current comparison period start date in YYYY-MM-DD format.');\nexport const currentEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Current comparison period end date in YYYY-MM-DD format.');\nexport const previousStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Previous comparison period start date in YYYY-MM-DD format.');\nexport const previousEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe('Previous comparison period end date in YYYY-MM-DD format.');\nexport const rowLimitSchemaField = z.number().int().min(1).max(25000).optional().describe('Maximum number of rows to return. Search Console supports up to 25,000 rows per request.');\nexport const startRowSchemaField = z.number().int().min(0).optional().describe('Zero-based row offset for paginated Search Console requests.');\nexport const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe('Search type to query. Defaults to web.');\nexport const brandTermsSchemaField = z.array(z.string().min(1)).max(50).optional().describe('Brand tokens used to separate branded from non-branded queries.');\n\nexport async function resolveSearchConsoleSiteUrl(siteUrl?: string, profileId?: string): Promise<string> {\n const explicitSiteUrl = siteUrl?.trim();\n if (explicitSiteUrl) return explicitSiteUrl;\n const explicitProfileId = profileId?.trim();\n if (explicitProfileId) {\n const mapping = await getProfileGoogleServiceMapping(explicitProfileId);\n if (mapping?.searchConsoleSiteUrl) return mapping.searchConsoleSiteUrl;\n throw new Error(`Missing Search Console site URL. profileId \"${explicitProfileId}\" has no Search Console mapping configured.`);\n }\n throw new Error('Missing Search Console site URL. Pass siteUrl explicitly, provide profileId with a configured mapping, or call gsc_list_accessible_sites first.');\n}\n\nexport function inferSearchConsolePropertyType(siteUrl: string): 'domain' | 'url_prefix' | 'unknown' { if (siteUrl.startsWith('sc-domain:')) return 'domain'; if (/^https?:\\/\\//.test(siteUrl)) return 'url_prefix'; return 'unknown'; }\nexport function normalizePermissionLevel(permissionLevel?: string): string | undefined { return permissionLevel?.trim().toLowerCase() || undefined; }\nexport function normalizeSearchConsoleSiteEntry(siteEntry: SearchConsoleSiteEntry) { return { site_url: siteEntry.siteUrl, property_type: inferSearchConsolePropertyType(siteEntry.siteUrl), permission_level: normalizePermissionLevel(siteEntry.permissionLevel) }; }\nexport function toPercent(numerator: number, denominator: number): number { if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return 0; return (numerator / denominator) * 100; }\nexport function round(value: number, decimals = 2): number { if (!Number.isFinite(value)) return 0; const factor = 10 ** decimals; return Math.round(value * factor) / factor; }\nexport function normalizeSearchConsoleRow(row: SearchConsoleSearchAnalyticsRow, dimensions: readonly string[] = []) { const clicks = row.clicks ?? 0; const impressions = row.impressions ?? 0; const ctr = typeof row.ctr === 'number' ? row.ctr * 100 : toPercent(clicks, impressions); const keys = row.keys ?? []; return { keys, dimensions: dimensions.reduce<Record<string, string>>((accumulator, dimension, index) => { accumulator[dimension] = keys[index] ?? ''; return accumulator; }, {}), clicks: round(clicks, 2), impressions: round(impressions, 2), ctr_percent: round(ctr, 2), position: round(row.position ?? 0, 2) }; }\nexport function buildSearchConsoleQueryBody(input: { startDate: string; endDate: string; dimensions?: string[]; searchType?: string; dimensionFilters?: SearchConsoleDimensionFilter[]; aggregationType?: string; rowLimit?: number; startRow?: number; }): SearchConsoleSearchAnalyticsRequest { const dimensionFilterGroups: SearchConsoleDimensionFilterGroup[] | undefined = input.dimensionFilters && input.dimensionFilters.length > 0 ? [{ groupType: 'and', filters: input.dimensionFilters }] : undefined; return { startDate: input.startDate, endDate: input.endDate, dimensions: input.dimensions, type: input.searchType ?? 'web', dimensionFilterGroups, aggregationType: input.aggregationType, rowLimit: input.rowLimit, startRow: input.startRow }; }\nexport function buildPaginationMetadata(input: { startRow?: number; rowLimit?: number; returnedRows: number; }) { const startRow = input.startRow ?? 0; const rowLimit = input.rowLimit; const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit; const nextStartRow = isPartial ? startRow + input.returnedRows : undefined; return { is_partial: isPartial, start_row: startRow, row_limit: rowLimit, returned_rows: input.returnedRows, next_start_row: nextStartRow, 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.' : undefined }; }\nexport function parseBrandTerms(brandTerms?: string[]): string[] { return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean); }\nexport function isBrandedQuery(query: string, brandTerms?: string[]): boolean { const normalizedQuery = query.trim().toLowerCase(); const normalizedBrandTerms = parseBrandTerms(brandTerms); if (!normalizedQuery || normalizedBrandTerms.length === 0) return false; return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm)); }\nexport function classifyBrandLabel(query: string, brandTerms?: string[]) { if ((brandTerms ?? []).length === 0) return 'unknown'; return isBrandedQuery(query, brandTerms) ? 'brand' : 'non_brand'; }\nexport function sumSearchConsoleRows(rows: Array<{ clicks: number; impressions: number; ctr_percent: number; position: number }>) { const clicks = rows.reduce((total, row) => total + row.clicks, 0); const impressions = rows.reduce((total, row) => total + row.impressions, 0); const weightedCtr = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.ctr_percent, 0) / impressions : 0; const weightedPosition = impressions > 0 ? rows.reduce((total, row) => total + row.impressions * row.position, 0) / impressions : 0; return { clicks: round(clicks), impressions: round(impressions), ctrPercent: round(weightedCtr), averagePosition: round(weightedPosition) }; }\nexport function scoreQuickWinOpportunity(input: { impressions: number; ctrPercent: number; position: number }) { return round(input.impressions * Math.max(0, 8 - input.ctrPercent) * Math.max(0.5, 20 - input.position) / 100); }\nexport function scoreHighImpressionLowClickOpportunity(input: { impressions: number; ctrPercent: number; position: number }) { return round(input.impressions * Math.max(0, 5 - input.ctrPercent) * Math.max(0.5, 15 - input.position) / 100); }\nexport function classifyQuickWinType(position: number, ctrPercent: number) { if (position > 0 && position <= 10 && ctrPercent < 4) return 'ctr'; if (position > 10 && position <= 20) return 'position'; return 'mixed'; }\nexport function looksLikeProductQuery(query: string, productTerms?: string[]) { if ((productTerms ?? []).some((term) => query.toLowerCase().includes(term.trim().toLowerCase()))) return true; return /\\b(comprar|precio|modelo|sku|talle|color|review|opiniones)\\b/i.test(query); }\nexport function buildComparisonMap<T>(rows: T[], getKey: (row: T) => string) { return new Map(rows.map((row) => [getKey(row), row])); }\nexport function computeChangePercent(current: number, previous: number) { if (previous === 0) return current > 0 ? 100 : 0; return round(((current - previous) / previous) * 100); }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -16,12 +16,13 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
16
16
|
const attributionGapsSchema = z.object({
|
|
17
17
|
startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
|
|
18
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,
|
|
19
|
+
propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
|
|
20
|
+
profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property."),
|
|
20
21
|
limit: z.number().int().min(10).max(100).optional().describe("Maximum source / medium rows to inspect")
|
|
21
22
|
});
|
|
22
23
|
async function attributionGapsHandler(params) {
|
|
23
24
|
try {
|
|
24
|
-
const propertyId = resolveGa4PropertyId(params.propertyId);
|
|
25
|
+
const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
|
|
25
26
|
const limit = params.limit ?? 50;
|
|
26
27
|
const [channelReport, sourceMediumReport, totalReport] = await Promise.all([
|
|
27
28
|
runReport(propertyId, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/tools/analytics/attribution-gaps.ts"],
|
|
4
|
-
"sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n detectSeverity,\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n isProblematicAttributionValue,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const attributionGapsSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted,
|
|
5
|
-
"mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,
|
|
4
|
+
"sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n detectSeverity,\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n isProblematicAttributionValue,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const attributionGapsSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID.\"),\n profileId: z.string().optional().describe(\"Optional business profile ID used to resolve the mapped GA4 property.\"),\n limit: z.number().int().min(10).max(100).optional().describe(\"Maximum source / medium rows to inspect\"),\n});\n\nexport async function attributionGapsHandler(params: z.infer<typeof attributionGapsSchema>) {\n try {\n const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);\n const limit = params.limit ?? 50;\n\n const [channelReport, sourceMediumReport, totalReport] = await Promise.all([\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionDefaultChannelGroup\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n }),\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionSourceMedium\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n orderBys: [{ metric: { metricName: \"sessions\" }, desc: true }],\n limit,\n returnPropertyQuota: true,\n }),\n runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n metrics: [{ name: \"sessions\" }],\n }),\n ]);\n\n const totalSessions = getMetricValueByName(\n totalReport.rows?.[0] ?? {},\n totalReport.metricHeaders,\n \"sessions\"\n );\n\n const unassignedChannel = (channelReport.rows ?? []).find(\n (row) => getDimensionValue(row) === \"Unassigned\"\n );\n const unassignedSessions = getMetricValueByName(\n unassignedChannel ?? {},\n channelReport.metricHeaders,\n \"sessions\"\n );\n\n const problematicSourceMediumRows = (sourceMediumReport.rows ?? [])\n .filter((row) => isProblematicAttributionValue(getDimensionValue(row)))\n .map((row) => {\n const sessions = getMetricValueByName(row, sourceMediumReport.metricHeaders, \"sessions\");\n const users = getMetricValueByName(row, sourceMediumReport.metricHeaders, \"totalUsers\");\n\n return {\n source_medium: getDimensionValue(row),\n sessions,\n users,\n session_share_percent: round(toPercent(sessions, totalSessions)),\n };\n });\n\n const problematicSourceMediumSessions = problematicSourceMediumRows.reduce(\n (total, row) => total + row.sessions,\n 0\n );\n const unassignedSharePercent = round(toPercent(unassignedSessions, totalSessions));\n const problematicSourceMediumSharePercent = round(\n toPercent(problematicSourceMediumSessions, totalSessions)\n );\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n overview: {\n total_sessions: totalSessions,\n attribution_warning: \"Las se\u00F1ales se reportan por separado para evitar doble conteo entre dimensiones distintas de GA4.\",\n },\n unassigned_channel_group: {\n sessions: unassignedSessions,\n session_share_percent: unassignedSharePercent,\n severity: detectSeverity(unassignedSharePercent),\n },\n problematic_source_medium: {\n sessions: problematicSourceMediumSessions,\n session_share_percent: problematicSourceMediumSharePercent,\n severity: detectSeverity(problematicSourceMediumSharePercent),\n },\n problematic_source_mediums: problematicSourceMediumRows,\n quota: extractQuotaSnapshot(sourceMediumReport),\n })\n );\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to detect GA4 attribution gaps\");\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -16,11 +16,12 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
16
16
|
const channelMixSchema = z.object({
|
|
17
17
|
startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
|
|
18
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,
|
|
19
|
+
propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
|
|
20
|
+
profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property.")
|
|
20
21
|
});
|
|
21
22
|
async function channelMixHandler(params) {
|
|
22
23
|
try {
|
|
23
|
-
const propertyId = resolveGa4PropertyId(params.propertyId);
|
|
24
|
+
const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
|
|
24
25
|
const report = await runReport(propertyId, {
|
|
25
26
|
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
26
27
|
dimensions: [{ name: "sessionDefaultChannelGroup" }],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/tools/analytics/channel-mix.ts"],
|
|
4
|
-
"sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n sumMetricValues,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { isPaidDefaultChannelGroup } from \"../../analytics/ga4-channel-groups.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const channelMixSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted,
|
|
5
|
-
"mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,
|
|
4
|
+
"sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getDimensionValue,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n sumMetricValues,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { isPaidDefaultChannelGroup } from \"../../analytics/ga4-channel-groups.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const channelMixSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID.\"),\n profileId: z.string().optional().describe(\"Optional business profile ID used to resolve the mapped GA4 property.\"),\n});\n\nexport async function channelMixHandler(params: z.infer<typeof channelMixSchema>) {\n try {\n const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);\n\n const report = await runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n dimensions: [{ name: \"sessionDefaultChannelGroup\" }],\n metrics: [{ name: \"sessions\" }, { name: \"totalUsers\" }],\n orderBys: [{ metric: { metricName: \"sessions\" }, desc: true }],\n returnPropertyQuota: true,\n });\n\n const totalSessions = sumMetricValues(report.rows, report.metricHeaders, \"sessions\");\n const totalUsers = sumMetricValues(report.rows, report.metricHeaders, \"totalUsers\");\n\n const channels = (report.rows ?? []).map((row) => {\n const sessions = getMetricValueByName(row, report.metricHeaders, \"sessions\");\n const users = getMetricValueByName(row, report.metricHeaders, \"totalUsers\");\n\n return {\n channel: getDimensionValue(row),\n sessions,\n users,\n session_share_percent: round(toPercent(sessions, totalSessions)),\n user_share_percent: round(toPercent(users, totalUsers)),\n };\n });\n\n const paidSessions = channels\n .filter((channel) => isPaidDefaultChannelGroup(channel.channel))\n .reduce((total, channel) => total + channel.sessions, 0);\n const organicSessions = channels\n .filter((channel) => channel.channel.startsWith(\"Organic\"))\n .reduce((total, channel) => total + channel.sessions, 0);\n const directSessions = channels\n .filter((channel) => channel.channel === \"Direct\")\n .reduce((total, channel) => total + channel.sessions, 0);\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n overview: {\n total_sessions: totalSessions,\n total_users: totalUsers,\n top_channel: channels[0]?.channel,\n paid_session_share_percent: round(toPercent(paidSessions, totalSessions)),\n organic_session_share_percent: round(toPercent(organicSessions, totalSessions)),\n direct_session_share_percent: round(toPercent(directSessions, totalSessions)),\n },\n channels,\n quota: extractQuotaSnapshot(report),\n })\n );\n } catch (err) {\n return error(err instanceof Error ? err.message : \"Failed to fetch GA4 channel mix\");\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,11 +13,12 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
13
13
|
const ecommerceTrackingHealthSchema = z.object({
|
|
14
14
|
startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
|
|
15
15
|
endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
|
|
16
|
-
propertyId: z.string().optional().describe("GA4 property ID. If omitted,
|
|
16
|
+
propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
|
|
17
|
+
profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property.")
|
|
17
18
|
});
|
|
18
19
|
async function ecommerceTrackingHealthHandler(params) {
|
|
19
20
|
try {
|
|
20
|
-
const propertyId = resolveGa4PropertyId(params.propertyId);
|
|
21
|
+
const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
|
|
21
22
|
const report = await runReport(propertyId, {
|
|
22
23
|
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
23
24
|
metrics: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/tools/analytics/ecommerce-tracking-health.ts"],
|
|
4
|
-
"sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const ecommerceTrackingHealthSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted,
|
|
5
|
-
"mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,
|
|
4
|
+
"sourcesContent": ["import { error, object } from \"mcp-use/server\";\nimport { z } from \"zod\";\n\nimport {\n extractQuotaSnapshot,\n getMetricValueByName,\n resolveGa4PropertyId,\n round,\n toPercent,\n} from \"../../analytics/ga4-report-utils.js\";\nimport { runReport } from \"../../services/analytics/ga4-client.js\";\nimport { stripNulls } from \"../../utils/strip-payload.js\";\n\nconst dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const ecommerceTrackingHealthSchema = z.object({\n startDate: z.string().regex(dateRegex).describe(\"Start date in YYYY-MM-DD format\"),\n endDate: z.string().regex(dateRegex).describe(\"End date in YYYY-MM-DD format\"),\n propertyId: z.string().optional().describe(\"GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID.\"),\n profileId: z.string().optional().describe(\"Optional business profile ID used to resolve the mapped GA4 property.\"),\n});\n\nexport async function ecommerceTrackingHealthHandler(\n params: z.infer<typeof ecommerceTrackingHealthSchema>\n) {\n try {\n const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);\n const report = await runReport(propertyId, {\n dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],\n metrics: [\n { name: \"itemViewEvents\" },\n { name: \"addToCarts\" },\n { name: \"checkouts\" },\n { name: \"transactions\" },\n { name: \"purchaseRevenue\" },\n { name: \"refundAmount\" },\n ],\n returnPropertyQuota: true,\n });\n\n const row = report.rows?.[0] ?? {};\n const itemViews = getMetricValueByName(row, report.metricHeaders, \"itemViewEvents\");\n const addToCarts = getMetricValueByName(row, report.metricHeaders, \"addToCarts\");\n const checkouts = getMetricValueByName(row, report.metricHeaders, \"checkouts\");\n const transactions = getMetricValueByName(row, report.metricHeaders, \"transactions\");\n const purchaseRevenue = getMetricValueByName(row, report.metricHeaders, \"purchaseRevenue\");\n const refundAmount = getMetricValueByName(row, report.metricHeaders, \"refundAmount\");\n\n const warnings: string[] = [];\n if (itemViews === 0) {\n warnings.push(\"No se detectaron view_item events en el rango consultado.\");\n }\n if (addToCarts === 0 && itemViews > 0) {\n warnings.push(\"Hay view_item pero no add_to_cart. Revisar tagging de add_to_cart.\");\n }\n if (checkouts === 0 && addToCarts > 0) {\n warnings.push(\"Hay add_to_cart pero no begin_checkout. Revisar continuidad del funnel.\");\n }\n if (transactions === 0 && checkouts > 0) {\n warnings.push(\"Hay begin_checkout pero no purchase/transactions. Revisar tagging de compra.\");\n }\n if (purchaseRevenue > 0 && transactions === 0) {\n warnings.push(\"Hay revenue pero no transactions. Revisar consistencia de ecommerce events.\");\n }\n\n return object(\n stripNulls({\n property_id: propertyId,\n date_range: {\n start_date: params.startDate,\n end_date: params.endDate,\n },\n currency_code: report.metadata?.currencyCode,\n funnel: {\n item_view_events: itemViews,\n add_to_carts: addToCarts,\n checkouts,\n transactions,\n purchase_revenue: round(purchaseRevenue),\n refund_amount: round(refundAmount),\n add_to_cart_rate_percent: round(toPercent(addToCarts, itemViews)),\n checkout_rate_percent: round(toPercent(checkouts, addToCarts)),\n purchase_rate_percent: round(toPercent(transactions, checkouts)),\n },\n warnings,\n quota: extractQuotaSnapshot(report),\n })\n );\n } catch (err) {\n return error(\n err instanceof Error ? err.message : \"Failed to evaluate GA4 ecommerce tracking health\"\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,OAAO,cAAc;AAC9B,SAAS,SAAS;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAEX,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,iCAAiC;AAAA,EACjF,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE,SAAS,+BAA+B;AAAA,EAC7E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,yFAAyF;AAAA,EACpI,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,uEAAuE;AACnH,CAAC;AAED,eAAsB,+BACpB,QACA;AACA,MAAI;AACF,UAAM,aAAa,MAAM,qBAAqB,OAAO,YAAY,OAAO,SAAS;AACjF,UAAM,SAAS,MAAM,UAAU,YAAY;AAAA,MACzC,YAAY,CAAC,EAAE,WAAW,OAAO,WAAW,SAAS,OAAO,QAAQ,CAAC;AAAA,MACrE,SAAS;AAAA,QACP,EAAE,MAAM,iBAAiB;AAAA,QACzB,EAAE,MAAM,aAAa;AAAA,QACrB,EAAE,MAAM,YAAY;AAAA,QACpB,EAAE,MAAM,eAAe;AAAA,QACvB,EAAE,MAAM,kBAAkB;AAAA,QAC1B,EAAE,MAAM,eAAe;AAAA,MACzB;AAAA,MACA,qBAAqB;AAAA,IACvB,CAAC;AAED,UAAM,MAAM,OAAO,OAAO,CAAC,KAAK,CAAC;AACjC,UAAM,YAAY,qBAAqB,KAAK,OAAO,eAAe,gBAAgB;AAClF,UAAM,aAAa,qBAAqB,KAAK,OAAO,eAAe,YAAY;AAC/E,UAAM,YAAY,qBAAqB,KAAK,OAAO,eAAe,WAAW;AAC7E,UAAM,eAAe,qBAAqB,KAAK,OAAO,eAAe,cAAc;AACnF,UAAM,kBAAkB,qBAAqB,KAAK,OAAO,eAAe,iBAAiB;AACzF,UAAM,eAAe,qBAAqB,KAAK,OAAO,eAAe,cAAc;AAEnF,UAAM,WAAqB,CAAC;AAC5B,QAAI,cAAc,GAAG;AACnB,eAAS,KAAK,2DAA2D;AAAA,IAC3E;AACA,QAAI,eAAe,KAAK,YAAY,GAAG;AACrC,eAAS,KAAK,oEAAoE;AAAA,IACpF;AACA,QAAI,cAAc,KAAK,aAAa,GAAG;AACrC,eAAS,KAAK,yEAAyE;AAAA,IACzF;AACA,QAAI,iBAAiB,KAAK,YAAY,GAAG;AACvC,eAAS,KAAK,8EAA8E;AAAA,IAC9F;AACA,QAAI,kBAAkB,KAAK,iBAAiB,GAAG;AAC7C,eAAS,KAAK,6EAA6E;AAAA,IAC7F;AAEA,WAAO;AAAA,MACL,WAAW;AAAA,QACT,aAAa;AAAA,QACb,YAAY;AAAA,UACV,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB;AAAA,QACA,eAAe,OAAO,UAAU;AAAA,QAChC,QAAQ;AAAA,UACN,kBAAkB;AAAA,UAClB,cAAc;AAAA,UACd;AAAA,UACA;AAAA,UACA,kBAAkB,MAAM,eAAe;AAAA,UACvC,eAAe,MAAM,YAAY;AAAA,UACjC,0BAA0B,MAAM,UAAU,YAAY,SAAS,CAAC;AAAA,UAChE,uBAAuB,MAAM,UAAU,WAAW,UAAU,CAAC;AAAA,UAC7D,uBAAuB,MAAM,UAAU,cAAc,SAAS,CAAC;AAAA,QACjE;AAAA,QACA;AAAA,QACA,OAAO,qBAAqB,MAAM;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,11 +12,12 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
12
12
|
const engagementOverviewSchema = z.object({
|
|
13
13
|
startDate: z.string().regex(dateRegex).describe("Start date in YYYY-MM-DD format"),
|
|
14
14
|
endDate: z.string().regex(dateRegex).describe("End date in YYYY-MM-DD format"),
|
|
15
|
-
propertyId: z.string().optional().describe("GA4 property ID. If omitted,
|
|
15
|
+
propertyId: z.string().optional().describe("GA4 property ID. If omitted, resolves profileId mapping first and then GA4_PROPERTY_ID."),
|
|
16
|
+
profileId: z.string().optional().describe("Optional business profile ID used to resolve the mapped GA4 property.")
|
|
16
17
|
});
|
|
17
18
|
async function engagementOverviewHandler(params) {
|
|
18
19
|
try {
|
|
19
|
-
const propertyId = resolveGa4PropertyId(params.propertyId);
|
|
20
|
+
const propertyId = await resolveGa4PropertyId(params.propertyId, params.profileId);
|
|
20
21
|
const report = await runReport(propertyId, {
|
|
21
22
|
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
22
23
|
metrics: [
|