@yourbright/emdash-analytics-plugin 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.js +168 -11
- package/dist/index.js +190 -31
- package/package.json +1 -1
package/dist/admin.js
CHANGED
|
@@ -273,6 +273,137 @@ function MetricTable({
|
|
|
273
273
|
] }, item.urlPath)) })
|
|
274
274
|
] }) });
|
|
275
275
|
}
|
|
276
|
+
function KpiDeltaCard({ metric }) {
|
|
277
|
+
return /* @__PURE__ */ jsx(
|
|
278
|
+
StatCard,
|
|
279
|
+
{
|
|
280
|
+
label: metric.label,
|
|
281
|
+
value: formatInteger(metric.current),
|
|
282
|
+
note: `vs prev ${formatSignedInteger(metric.delta)} (${formatInteger(metric.previous)})`
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function TrendPanel({
|
|
287
|
+
title,
|
|
288
|
+
subtitle,
|
|
289
|
+
metrics,
|
|
290
|
+
trend
|
|
291
|
+
}) {
|
|
292
|
+
return /* @__PURE__ */ jsx(Section, { title, subtitle, children: /* @__PURE__ */ jsx("div", { className: "grid gap-4 lg:grid-cols-2", children: metrics.map((metric) => /* @__PURE__ */ jsx(TrendMetricCard, { metric, trend }, metric.key)) }) });
|
|
293
|
+
}
|
|
294
|
+
function TrendMetricCard({
|
|
295
|
+
metric,
|
|
296
|
+
trend
|
|
297
|
+
}) {
|
|
298
|
+
const data = trend.map((row) => ({
|
|
299
|
+
date: row.date,
|
|
300
|
+
value: metric.key === "gscClicks" ? row.gscClicks : metric.key === "gscImpressions" ? row.gscImpressions : metric.key === "gaViews" ? row.gaViews : metric.key === "gaUsers" ? row.gaUsers : row.gaSessions
|
|
301
|
+
}));
|
|
302
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border bg-background p-4", children: [
|
|
303
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
304
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
305
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: metric.label }),
|
|
306
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-2xl font-semibold", children: formatInteger(metric.current) })
|
|
307
|
+
] }),
|
|
308
|
+
/* @__PURE__ */ jsx("div", { className: `text-sm font-medium ${metric.delta >= 0 ? "text-emerald-700" : "text-red-700"}`, children: formatSignedInteger(metric.delta) })
|
|
309
|
+
] }),
|
|
310
|
+
/* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(Sparkline, { data }) }),
|
|
311
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 text-xs text-muted-foreground", children: [
|
|
312
|
+
"Previous 28d: ",
|
|
313
|
+
formatInteger(metric.previous)
|
|
314
|
+
] })
|
|
315
|
+
] });
|
|
316
|
+
}
|
|
317
|
+
function Sparkline({
|
|
318
|
+
data
|
|
319
|
+
}) {
|
|
320
|
+
if (data.length === 0) {
|
|
321
|
+
return /* @__PURE__ */ jsx("div", { className: "h-28 rounded-lg border border-dashed border-border bg-card" });
|
|
322
|
+
}
|
|
323
|
+
const width = 320;
|
|
324
|
+
const height = 112;
|
|
325
|
+
const padding = 10;
|
|
326
|
+
const values = data.map((item) => item.value);
|
|
327
|
+
const max = Math.max(...values, 1);
|
|
328
|
+
const min = Math.min(...values, 0);
|
|
329
|
+
const range = Math.max(max - min, 1);
|
|
330
|
+
const points = data.map((item, index) => {
|
|
331
|
+
const x = padding + index / Math.max(data.length - 1, 1) * (width - padding * 2);
|
|
332
|
+
const y = height - padding - (item.value - min) / range * (height - padding * 2);
|
|
333
|
+
return `${x},${y}`;
|
|
334
|
+
});
|
|
335
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${width} ${height}`, className: "h-28 w-full overflow-visible", children: /* @__PURE__ */ jsx(
|
|
336
|
+
"path",
|
|
337
|
+
{
|
|
338
|
+
d: `M ${points.join(" L ")}`,
|
|
339
|
+
fill: "none",
|
|
340
|
+
stroke: "var(--color-kumo-brand)",
|
|
341
|
+
strokeWidth: "2.5",
|
|
342
|
+
strokeLinecap: "round",
|
|
343
|
+
strokeLinejoin: "round"
|
|
344
|
+
}
|
|
345
|
+
) });
|
|
346
|
+
}
|
|
347
|
+
function BreakdownTable({
|
|
348
|
+
rows,
|
|
349
|
+
emptyMessage
|
|
350
|
+
}) {
|
|
351
|
+
if (rows.length === 0) {
|
|
352
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: emptyMessage });
|
|
353
|
+
}
|
|
354
|
+
return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-sm", children: [
|
|
355
|
+
/* @__PURE__ */ jsx("thead", { className: "text-left text-xs uppercase tracking-[0.16em] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
356
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Group" }),
|
|
357
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Pages" }),
|
|
358
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GSC Clicks" }),
|
|
359
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GA Views" }),
|
|
360
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GA Sessions" })
|
|
361
|
+
] }) }),
|
|
362
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/80", children: [
|
|
363
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4 font-medium", children: row.label }),
|
|
364
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatInteger(row.trackedPages) }),
|
|
365
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
366
|
+
formatInteger(row.current.gscClicks),
|
|
367
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatSignedInteger(row.delta.gscClicks) })
|
|
368
|
+
] }),
|
|
369
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
370
|
+
formatInteger(row.current.gaViews),
|
|
371
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatSignedInteger(row.delta.gaViews) })
|
|
372
|
+
] }),
|
|
373
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
374
|
+
formatInteger(row.current.gaSessions),
|
|
375
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatSignedInteger(row.delta.gaSessions) })
|
|
376
|
+
] })
|
|
377
|
+
] }, row.key)) })
|
|
378
|
+
] }) });
|
|
379
|
+
}
|
|
380
|
+
function MoversTable({
|
|
381
|
+
items,
|
|
382
|
+
emptyMessage
|
|
383
|
+
}) {
|
|
384
|
+
if (items.length === 0) {
|
|
385
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: emptyMessage });
|
|
386
|
+
}
|
|
387
|
+
return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full text-sm", children: [
|
|
388
|
+
/* @__PURE__ */ jsx("thead", { className: "text-left text-xs uppercase tracking-[0.16em] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
389
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Page" }),
|
|
390
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Type" }),
|
|
391
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GA Views \u0394" }),
|
|
392
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "GSC Clicks \u0394" }),
|
|
393
|
+
/* @__PURE__ */ jsx("th", { className: "pb-3 pr-4", children: "Current Views" })
|
|
394
|
+
] }) }),
|
|
395
|
+
/* @__PURE__ */ jsx("tbody", { children: items.map((item) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/80", children: [
|
|
396
|
+
/* @__PURE__ */ jsxs("td", { className: "py-3 pr-4", children: [
|
|
397
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium", children: item.title }),
|
|
398
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: item.urlPath })
|
|
399
|
+
] }),
|
|
400
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: pageKindLabel(item.pageKind) }),
|
|
401
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatSignedInteger(item.gaViewsDelta) }),
|
|
402
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatSignedInteger(item.gscClicksDelta) }),
|
|
403
|
+
/* @__PURE__ */ jsx("td", { className: "py-3 pr-4", children: formatInteger(item.gaViews28d) })
|
|
404
|
+
] }, item.urlPath)) })
|
|
405
|
+
] }) });
|
|
406
|
+
}
|
|
276
407
|
function OverviewPage() {
|
|
277
408
|
const [status, setStatus] = React.useState(null);
|
|
278
409
|
const [overview, setOverview] = React.useState(null);
|
|
@@ -307,7 +438,7 @@ function OverviewPage() {
|
|
|
307
438
|
Shell,
|
|
308
439
|
{
|
|
309
440
|
title: "Content Insights",
|
|
310
|
-
description: "
|
|
441
|
+
description: "Monitor site health, compare the last 28 days to the previous window, and spot pages that changed fastest.",
|
|
311
442
|
actions: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Reload" }),
|
|
312
443
|
children: [
|
|
313
444
|
/* @__PURE__ */ jsx(ErrorBanner, { message: error }),
|
|
@@ -318,17 +449,38 @@ function OverviewPage() {
|
|
|
318
449
|
/* @__PURE__ */ jsx(StatCard, { label: "GA Final Date", value: freshness.lastGaDate || "-" }),
|
|
319
450
|
/* @__PURE__ */ jsx(StatCard, { label: "Service Account", value: status?.config?.serviceAccountEmail || "-" })
|
|
320
451
|
] }) }),
|
|
321
|
-
/* @__PURE__ */ jsx(Section, { title: "KPI Snapshot", subtitle: "
|
|
322
|
-
|
|
323
|
-
/* @__PURE__ */ jsx(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
/* @__PURE__ */ jsx(Section, { title: "KPI Snapshot", subtitle: "Current 28 days versus the previous 28 days across all tracked public pages.", children: /* @__PURE__ */ jsx("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-5", children: (overview?.kpiDeltas ?? []).map((metric) => /* @__PURE__ */ jsx(KpiDeltaCard, { metric }, metric.key)) }) }),
|
|
453
|
+
summary ? /* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
454
|
+
/* @__PURE__ */ jsx(
|
|
455
|
+
TrendPanel,
|
|
456
|
+
{
|
|
457
|
+
title: "Search Trend",
|
|
458
|
+
subtitle: "Daily search demand and click capture for the current 28-day window.",
|
|
459
|
+
metrics: (overview?.kpiDeltas ?? []).filter(
|
|
460
|
+
(metric) => metric.key === "gscClicks" || metric.key === "gscImpressions"
|
|
461
|
+
),
|
|
462
|
+
trend: summary.trend
|
|
463
|
+
}
|
|
464
|
+
),
|
|
465
|
+
/* @__PURE__ */ jsx(
|
|
466
|
+
TrendPanel,
|
|
467
|
+
{
|
|
468
|
+
title: "Traffic Trend",
|
|
469
|
+
subtitle: "Daily traffic movement from GA4 for the current 28-day window.",
|
|
470
|
+
metrics: (overview?.kpiDeltas ?? []).filter(
|
|
471
|
+
(metric) => metric.key === "gaViews" || metric.key === "gaSessions"
|
|
472
|
+
),
|
|
473
|
+
trend: summary.trend
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
] }) : null,
|
|
329
477
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
330
|
-
/* @__PURE__ */ jsx(Section, { title: "
|
|
331
|
-
/* @__PURE__ */ jsx(Section, { title: "
|
|
478
|
+
/* @__PURE__ */ jsx(Section, { title: "Page Mix", subtitle: "Compare page groups by current volume and change from the previous window.", children: /* @__PURE__ */ jsx(BreakdownTable, { rows: overview?.pageKindBreakdown ?? [], emptyMessage: "No tracked pages yet." }) }),
|
|
479
|
+
/* @__PURE__ */ jsx(Section, { title: "Managed Coverage", subtitle: "See whether growth is coming from EmDash-managed content or unmanaged pages.", children: /* @__PURE__ */ jsx(BreakdownTable, { rows: overview?.managedBreakdown ?? [], emptyMessage: "No tracked pages yet." }) })
|
|
480
|
+
] }),
|
|
481
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [
|
|
482
|
+
/* @__PURE__ */ jsx(Section, { title: "Top Gainers", subtitle: "Pages with the strongest positive movement in the last 28 days.", children: /* @__PURE__ */ jsx(MoversTable, { items: overview?.topGainers ?? [], emptyMessage: "No gaining pages yet." }) }),
|
|
483
|
+
/* @__PURE__ */ jsx(Section, { title: "Top Decliners", subtitle: "Pages with the sharpest drop and the clearest candidates for investigation.", children: /* @__PURE__ */ jsx(MoversTable, { items: overview?.topDecliners ?? [], emptyMessage: "No declining pages yet." }) })
|
|
332
484
|
] }),
|
|
333
485
|
/* @__PURE__ */ jsx(Section, { title: "Reporting Windows", subtitle: "The agent API returns the same windows.", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
334
486
|
/* @__PURE__ */ jsx(WindowCard, { label: "GSC Current", value: summary?.window.gscCurrent }),
|
|
@@ -789,6 +941,11 @@ function WindowCard({
|
|
|
789
941
|
function formatInteger(value) {
|
|
790
942
|
return new Intl.NumberFormat("ja-JP").format(value ?? 0);
|
|
791
943
|
}
|
|
944
|
+
function formatSignedInteger(value) {
|
|
945
|
+
const numeric = value ?? 0;
|
|
946
|
+
if (numeric === 0) return "0";
|
|
947
|
+
return `${numeric > 0 ? "+" : ""}${formatInteger(numeric)}`;
|
|
948
|
+
}
|
|
792
949
|
function formatPercent(value) {
|
|
793
950
|
return `${((value ?? 0) * 100).toFixed(1)}%`;
|
|
794
951
|
}
|
package/dist/index.js
CHANGED
|
@@ -264,31 +264,36 @@ function buildContentUrl(siteOrigin, urlPath) {
|
|
|
264
264
|
return new URL(urlPath, `${siteOrigin}/`).toString();
|
|
265
265
|
}
|
|
266
266
|
async function getManagedContentMap(siteOrigin) {
|
|
267
|
-
const result = await getEmDashCollection("posts", {
|
|
268
|
-
status: "published",
|
|
269
|
-
limit: 1e3,
|
|
270
|
-
orderBy: { updatedAt: "desc" }
|
|
271
|
-
});
|
|
272
267
|
const managed = /* @__PURE__ */ new Map();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const seoDescription = typeof data.seo_description === "string" ? data.seo_description : void 0;
|
|
281
|
-
const urlPath = `/blog/${slug || id}/`;
|
|
282
|
-
managed.set(urlPath, {
|
|
283
|
-
collection: "posts",
|
|
284
|
-
id,
|
|
285
|
-
slug,
|
|
286
|
-
urlPath,
|
|
287
|
-
title,
|
|
288
|
-
excerpt,
|
|
289
|
-
seoDescription
|
|
268
|
+
let cursor;
|
|
269
|
+
do {
|
|
270
|
+
const result = await getEmDashCollection("posts", {
|
|
271
|
+
status: "published",
|
|
272
|
+
limit: 50,
|
|
273
|
+
cursor,
|
|
274
|
+
orderBy: { updatedAt: "desc" }
|
|
290
275
|
});
|
|
291
|
-
|
|
276
|
+
for (const entry of result.entries) {
|
|
277
|
+
const id = typeof entry.id === "string" ? entry.id : "";
|
|
278
|
+
if (!id) continue;
|
|
279
|
+
const slug = typeof entry.slug === "string" ? entry.slug : null;
|
|
280
|
+
const data = entry.data ?? {};
|
|
281
|
+
const title = typeof data.title === "string" ? data.title : slug || id;
|
|
282
|
+
const excerpt = typeof data.excerpt === "string" ? data.excerpt : void 0;
|
|
283
|
+
const seoDescription = typeof data.seo_description === "string" ? data.seo_description : void 0;
|
|
284
|
+
const urlPath = `/blog/${slug || id}/`;
|
|
285
|
+
managed.set(urlPath, {
|
|
286
|
+
collection: "posts",
|
|
287
|
+
id,
|
|
288
|
+
slug,
|
|
289
|
+
urlPath,
|
|
290
|
+
title,
|
|
291
|
+
excerpt,
|
|
292
|
+
seoDescription
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
cursor = result.nextCursor;
|
|
296
|
+
} while (cursor);
|
|
292
297
|
void siteOrigin;
|
|
293
298
|
return managed;
|
|
294
299
|
}
|
|
@@ -997,7 +1002,7 @@ async function listPages(ctx, filters) {
|
|
|
997
1002
|
};
|
|
998
1003
|
}
|
|
999
1004
|
async function getOverview(ctx) {
|
|
1000
|
-
const [summary, freshness, topOpportunities, topUnmanaged] = await Promise.all([
|
|
1005
|
+
const [summary, freshness, topOpportunities, topUnmanaged, allPages] = await Promise.all([
|
|
1001
1006
|
ctx.kv.get(SITE_SUMMARY_KEY),
|
|
1002
1007
|
getFreshness(ctx),
|
|
1003
1008
|
ctx.storage.pages.query({
|
|
@@ -1009,13 +1014,36 @@ async function getOverview(ctx) {
|
|
|
1009
1014
|
where: { managed: false },
|
|
1010
1015
|
orderBy: { gaViews28d: "desc" },
|
|
1011
1016
|
limit: 5
|
|
1012
|
-
})
|
|
1017
|
+
}),
|
|
1018
|
+
listAllPages(ctx)
|
|
1013
1019
|
]);
|
|
1020
|
+
return buildOverviewData(
|
|
1021
|
+
summary,
|
|
1022
|
+
freshness,
|
|
1023
|
+
allPages,
|
|
1024
|
+
topOpportunities.items.map((item) => item.data),
|
|
1025
|
+
topUnmanaged.items.map((item) => item.data)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
function buildOverviewData(summary, freshness, allPages, topOpportunities, topUnmanaged) {
|
|
1014
1029
|
return {
|
|
1015
1030
|
summary,
|
|
1016
1031
|
freshness,
|
|
1017
|
-
|
|
1018
|
-
|
|
1032
|
+
kpiDeltas: buildKpiDeltas(allPages),
|
|
1033
|
+
pageKindBreakdown: buildBreakdown(
|
|
1034
|
+
allPages,
|
|
1035
|
+
(page) => page.pageKind,
|
|
1036
|
+
(key) => pageKindLabel(key)
|
|
1037
|
+
),
|
|
1038
|
+
managedBreakdown: buildBreakdown(
|
|
1039
|
+
allPages,
|
|
1040
|
+
(page) => page.managed ? "managed" : "unmanaged",
|
|
1041
|
+
(key) => key === "managed" ? "Managed" : "Unmanaged"
|
|
1042
|
+
),
|
|
1043
|
+
topGainers: buildMovers(allPages, "gainers"),
|
|
1044
|
+
topDecliners: buildMovers(allPages, "decliners"),
|
|
1045
|
+
topOpportunities,
|
|
1046
|
+
topUnmanaged
|
|
1019
1047
|
};
|
|
1020
1048
|
}
|
|
1021
1049
|
async function getContentContext(ctx, collection, id, slug) {
|
|
@@ -1235,6 +1263,19 @@ async function getFreshness(ctx) {
|
|
|
1235
1263
|
lastStatus: "idle"
|
|
1236
1264
|
};
|
|
1237
1265
|
}
|
|
1266
|
+
async function listAllPages(ctx) {
|
|
1267
|
+
const pages = [];
|
|
1268
|
+
let cursor;
|
|
1269
|
+
do {
|
|
1270
|
+
const batch = await ctx.storage.pages.query({
|
|
1271
|
+
limit: 500,
|
|
1272
|
+
cursor
|
|
1273
|
+
});
|
|
1274
|
+
cursor = batch.cursor;
|
|
1275
|
+
pages.push(...batch.items.map((item) => item.data));
|
|
1276
|
+
} while (cursor);
|
|
1277
|
+
return pages;
|
|
1278
|
+
}
|
|
1238
1279
|
async function refreshSummaryFromStorage(ctx) {
|
|
1239
1280
|
const windows = buildWindows();
|
|
1240
1281
|
const allPages = [];
|
|
@@ -1327,6 +1368,112 @@ function mergeTrend(gscTrend, gaTrend) {
|
|
|
1327
1368
|
}
|
|
1328
1369
|
return Array.from(map.values()).sort((left, right) => left.date.localeCompare(right.date));
|
|
1329
1370
|
}
|
|
1371
|
+
function buildKpiDeltas(pages) {
|
|
1372
|
+
const metrics = [
|
|
1373
|
+
{
|
|
1374
|
+
key: "gscClicks",
|
|
1375
|
+
label: "GSC Clicks",
|
|
1376
|
+
current: sumPages(pages, (page) => page.gscClicks28d),
|
|
1377
|
+
previous: sumPages(pages, (page) => page.gscClicksPrev28d)
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
key: "gscImpressions",
|
|
1381
|
+
label: "GSC Impressions",
|
|
1382
|
+
current: sumPages(pages, (page) => page.gscImpressions28d),
|
|
1383
|
+
previous: sumPages(pages, (page) => page.gscImpressionsPrev28d)
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
key: "gaViews",
|
|
1387
|
+
label: "GA Views",
|
|
1388
|
+
current: sumPages(pages, (page) => page.gaViews28d),
|
|
1389
|
+
previous: sumPages(pages, (page) => page.gaViewsPrev28d)
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
key: "gaUsers",
|
|
1393
|
+
label: "GA Users",
|
|
1394
|
+
current: sumPages(pages, (page) => page.gaUsers28d),
|
|
1395
|
+
previous: sumPages(pages, (page) => page.gaUsersPrev28d)
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
key: "gaSessions",
|
|
1399
|
+
label: "GA Sessions",
|
|
1400
|
+
current: sumPages(pages, (page) => page.gaSessions28d),
|
|
1401
|
+
previous: sumPages(pages, (page) => page.gaSessionsPrev28d)
|
|
1402
|
+
}
|
|
1403
|
+
];
|
|
1404
|
+
return metrics.map((metric) => ({
|
|
1405
|
+
...metric,
|
|
1406
|
+
delta: metric.current - metric.previous
|
|
1407
|
+
}));
|
|
1408
|
+
}
|
|
1409
|
+
function buildBreakdown(pages, getKey, getLabel) {
|
|
1410
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1411
|
+
for (const page of pages) {
|
|
1412
|
+
const key = getKey(page);
|
|
1413
|
+
const existing = buckets.get(key) ?? {
|
|
1414
|
+
key,
|
|
1415
|
+
label: getLabel(key),
|
|
1416
|
+
trackedPages: 0,
|
|
1417
|
+
current: { gscClicks: 0, gaViews: 0, gaSessions: 0 },
|
|
1418
|
+
previous: { gscClicks: 0, gaViews: 0, gaSessions: 0 },
|
|
1419
|
+
delta: { gscClicks: 0, gaViews: 0, gaSessions: 0 }
|
|
1420
|
+
};
|
|
1421
|
+
existing.trackedPages += 1;
|
|
1422
|
+
existing.current.gscClicks += page.gscClicks28d;
|
|
1423
|
+
existing.current.gaViews += page.gaViews28d;
|
|
1424
|
+
existing.current.gaSessions += page.gaSessions28d;
|
|
1425
|
+
existing.previous.gscClicks += page.gscClicksPrev28d;
|
|
1426
|
+
existing.previous.gaViews += page.gaViewsPrev28d;
|
|
1427
|
+
existing.previous.gaSessions += page.gaSessionsPrev28d;
|
|
1428
|
+
existing.delta.gscClicks = existing.current.gscClicks - existing.previous.gscClicks;
|
|
1429
|
+
existing.delta.gaViews = existing.current.gaViews - existing.previous.gaViews;
|
|
1430
|
+
existing.delta.gaSessions = existing.current.gaSessions - existing.previous.gaSessions;
|
|
1431
|
+
buckets.set(key, existing);
|
|
1432
|
+
}
|
|
1433
|
+
return Array.from(buckets.values()).sort((left, right) => right.current.gaViews - left.current.gaViews);
|
|
1434
|
+
}
|
|
1435
|
+
function buildMovers(pages, direction) {
|
|
1436
|
+
const rows = pages.map((page) => ({
|
|
1437
|
+
urlPath: page.urlPath,
|
|
1438
|
+
title: page.title,
|
|
1439
|
+
pageKind: page.pageKind,
|
|
1440
|
+
managed: page.managed,
|
|
1441
|
+
gscClicks28d: page.gscClicks28d,
|
|
1442
|
+
gaViews28d: page.gaViews28d,
|
|
1443
|
+
gscClicksDelta: page.gscClicks28d - page.gscClicksPrev28d,
|
|
1444
|
+
gaViewsDelta: page.gaViews28d - page.gaViewsPrev28d,
|
|
1445
|
+
opportunityScore: page.opportunityScore
|
|
1446
|
+
}));
|
|
1447
|
+
const filtered = rows.filter(
|
|
1448
|
+
(row) => direction === "gainers" ? row.gaViewsDelta > 0 || row.gscClicksDelta > 0 : row.gaViewsDelta < 0 || row.gscClicksDelta < 0
|
|
1449
|
+
);
|
|
1450
|
+
filtered.sort((left, right) => {
|
|
1451
|
+
if (direction === "gainers") {
|
|
1452
|
+
return right.gaViewsDelta - left.gaViewsDelta || right.gscClicksDelta - left.gscClicksDelta || right.gaViews28d - left.gaViews28d;
|
|
1453
|
+
}
|
|
1454
|
+
return left.gaViewsDelta - right.gaViewsDelta || left.gscClicksDelta - right.gscClicksDelta || right.gaViews28d - left.gaViews28d;
|
|
1455
|
+
});
|
|
1456
|
+
return filtered.slice(0, 5);
|
|
1457
|
+
}
|
|
1458
|
+
function sumPages(pages, getValue) {
|
|
1459
|
+
return pages.reduce((total, page) => total + getValue(page), 0);
|
|
1460
|
+
}
|
|
1461
|
+
function pageKindLabel(pageKind) {
|
|
1462
|
+
switch (pageKind) {
|
|
1463
|
+
case "blog_post":
|
|
1464
|
+
return "Blog Post";
|
|
1465
|
+
case "blog_archive":
|
|
1466
|
+
return "Blog Archive";
|
|
1467
|
+
case "tag":
|
|
1468
|
+
return "Tag";
|
|
1469
|
+
case "author":
|
|
1470
|
+
return "Author";
|
|
1471
|
+
case "landing":
|
|
1472
|
+
return "Landing";
|
|
1473
|
+
default:
|
|
1474
|
+
return "Other";
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1330
1477
|
|
|
1331
1478
|
// src/index.ts
|
|
1332
1479
|
var configSaveSchema = z2.object({
|
|
@@ -1473,14 +1620,26 @@ function createPlugin() {
|
|
|
1473
1620
|
if (!resolved.success) {
|
|
1474
1621
|
throw new PluginRouteError2("BAD_REQUEST", resolved.message, 400);
|
|
1475
1622
|
}
|
|
1476
|
-
|
|
1623
|
+
try {
|
|
1624
|
+
return testConnection(ctx, resolved.data);
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
const message = error instanceof Error ? error.message : "Connection test failed";
|
|
1627
|
+
console.error("[analytics-plugin] connection test failed", error);
|
|
1628
|
+
throw new PluginRouteError2("INTERNAL_ERROR", message, 500);
|
|
1629
|
+
}
|
|
1477
1630
|
}
|
|
1478
1631
|
},
|
|
1479
1632
|
[ADMIN_ROUTES.SYNC_NOW]: {
|
|
1480
1633
|
handler: async (ctx) => {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1634
|
+
try {
|
|
1635
|
+
const base = await syncBase(ctx, "manual");
|
|
1636
|
+
const enriched = await enrichManagedQueries(ctx);
|
|
1637
|
+
return { ...base, ...enriched };
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
const message = error instanceof Error ? error.message : "Manual sync failed";
|
|
1640
|
+
console.error("[analytics-plugin] manual sync failed", error);
|
|
1641
|
+
throw new PluginRouteError2("INTERNAL_ERROR", message, 500);
|
|
1642
|
+
}
|
|
1484
1643
|
}
|
|
1485
1644
|
},
|
|
1486
1645
|
[ADMIN_ROUTES.AGENT_KEYS_LIST]: {
|