@ainyc/canonry 3.5.0 → 3.6.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/assets/agent-workspace/skills/aero/references/reporting.md +1 -1
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +1 -1
- package/assets/assets/{index-CFtdvSnQ.js → index-B5VNd16q.js} +114 -114
- package/assets/index.html +1 -1
- package/dist/{chunk-5G7S6SEP.js → chunk-CG4HEQAK.js} +225 -35
- package/dist/{chunk-ZYESHCMF.js → chunk-GLPZ5NVP.js} +2 -2
- package/dist/{chunk-VIUWGDDU.js → chunk-RDX6GBWM.js} +12 -0
- package/dist/{chunk-K33FVWFW.js → chunk-W463NVVC.js} +43 -1
- package/dist/cli.js +5 -5
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-NGA6RLCH.js → intelligence-service-WZUM3AX6.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
package/assets/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-B5VNd16q.js"></script>
|
|
16
16
|
<link rel="stylesheet" crossorigin href="./assets/index-BfwQqd05.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
configExists,
|
|
5
5
|
loadConfig,
|
|
6
6
|
saveConfigPatch
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GLPZ5NVP.js";
|
|
8
8
|
import {
|
|
9
9
|
IntelligenceService,
|
|
10
10
|
MIN_TREND_POINTS,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
gaSocialReferrals,
|
|
32
32
|
gaTrafficSnapshots,
|
|
33
33
|
gaTrafficSummaries,
|
|
34
|
+
gaTrafficWindowSummaries,
|
|
34
35
|
groupInsights,
|
|
35
36
|
gscCoverageSnapshots,
|
|
36
37
|
gscSearchData,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
runs,
|
|
49
50
|
schedules,
|
|
50
51
|
usageCounters
|
|
51
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-W463NVVC.js";
|
|
52
53
|
import {
|
|
53
54
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
54
55
|
AGENT_PROVIDER_IDS,
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
RunKinds,
|
|
64
65
|
RunStatuses,
|
|
65
66
|
RunTriggers,
|
|
67
|
+
absolutizeProjectUrl,
|
|
66
68
|
agentBusy,
|
|
67
69
|
agentMemoryDeleteRequestSchema,
|
|
68
70
|
agentMemoryUpsertRequestSchema,
|
|
@@ -110,7 +112,7 @@ import {
|
|
|
110
112
|
visibilityStateFromAnswerMentioned,
|
|
111
113
|
windowCutoff,
|
|
112
114
|
wordpressEnvSchema
|
|
113
|
-
} from "./chunk-
|
|
115
|
+
} from "./chunk-RDX6GBWM.js";
|
|
114
116
|
|
|
115
117
|
// src/telemetry.ts
|
|
116
118
|
import crypto from "crypto";
|
|
@@ -2782,7 +2784,12 @@ function renderExecutiveSummary(report) {
|
|
|
2782
2784
|
<span>${escapeHtml(f.detail)}</span>
|
|
2783
2785
|
</div>`).join("")}</div>` : "";
|
|
2784
2786
|
return section(
|
|
2785
|
-
{
|
|
2787
|
+
{
|
|
2788
|
+
id: "executive-summary",
|
|
2789
|
+
eyebrow: "Section 1",
|
|
2790
|
+
title: "Executive Summary",
|
|
2791
|
+
intro: "Top-line citation rate with trend versus the prior run, plus the most actionable findings from the latest visibility sweep."
|
|
2792
|
+
},
|
|
2786
2793
|
metricsHtml + findingsHtml
|
|
2787
2794
|
);
|
|
2788
2795
|
}
|
|
@@ -2841,15 +2848,11 @@ function renderCitationScorecard(report) {
|
|
|
2841
2848
|
${renderCitationMatrix(report.citationScorecard)}
|
|
2842
2849
|
`;
|
|
2843
2850
|
return section(
|
|
2844
|
-
{ id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "
|
|
2851
|
+
{ id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Whether your domain appeared in each AI engine\u2019s source list for every tracked keyword in the latest sweep \u2014 a cell turns green when your domain was cited, red when it was not, and gray when no snapshot exists for that pair." },
|
|
2845
2852
|
body
|
|
2846
2853
|
);
|
|
2847
2854
|
}
|
|
2848
|
-
function
|
|
2849
|
-
const data = [
|
|
2850
|
-
{ label: canonical, count: landscape.projectCitationCount, isProject: true },
|
|
2851
|
-
...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
|
|
2852
|
-
];
|
|
2855
|
+
function renderLandscapeBars(data, heading, ariaLabel) {
|
|
2853
2856
|
if (data.length <= 1) return "";
|
|
2854
2857
|
const max = Math.max(...data.map((d) => d.count), 1);
|
|
2855
2858
|
const width = 600;
|
|
@@ -2866,22 +2869,43 @@ function renderCompetitorBars(landscape, canonical) {
|
|
|
2866
2869
|
<text x="${labelWidth + w + 6}" y="${y + 13}" fill="${COLORS.text}" font-size="11">${d.count}</text>`;
|
|
2867
2870
|
}).join("");
|
|
2868
2871
|
return `<div class="chart-card">
|
|
2869
|
-
<h3
|
|
2870
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="
|
|
2872
|
+
<h3>${escapeHtml(heading)}</h3>
|
|
2873
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml(ariaLabel)}">
|
|
2871
2874
|
${bars}
|
|
2872
2875
|
</svg>
|
|
2873
2876
|
</div>`;
|
|
2874
2877
|
}
|
|
2878
|
+
function renderCompetitorBars(landscape, canonical) {
|
|
2879
|
+
const data = [
|
|
2880
|
+
{ label: canonical, count: landscape.projectCitationCount, isProject: true },
|
|
2881
|
+
...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
|
|
2882
|
+
];
|
|
2883
|
+
return renderLandscapeBars(data, "Citations per domain", "Citations per domain bar chart");
|
|
2884
|
+
}
|
|
2885
|
+
function renderMentionBars(landscape, canonical) {
|
|
2886
|
+
const data = [
|
|
2887
|
+
{ label: canonical, count: landscape.projectMentionCount, isProject: true },
|
|
2888
|
+
...landscape.competitors.map((c) => ({ label: c.domain, count: c.mentionCount, isProject: false }))
|
|
2889
|
+
];
|
|
2890
|
+
return renderLandscapeBars(data, "Mentions per domain", "Mentions per domain bar chart");
|
|
2891
|
+
}
|
|
2875
2892
|
function renderCompetitorLandscape(report) {
|
|
2876
2893
|
const competitors2 = report.competitorLandscape.competitors;
|
|
2877
|
-
|
|
2894
|
+
const mentionLandscape = report.mentionLandscape;
|
|
2895
|
+
const noCitationData = competitors2.length === 0 && report.competitorLandscape.projectCitationCount === 0;
|
|
2896
|
+
const noMentionData = mentionLandscape.competitors.length === 0 && mentionLandscape.projectMentionCount === 0;
|
|
2897
|
+
if (noCitationData && noMentionData) {
|
|
2878
2898
|
return section(
|
|
2879
2899
|
{ id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape" },
|
|
2880
2900
|
renderEmpty("No competitor data yet. Add competitors and run a visibility sweep.")
|
|
2881
2901
|
);
|
|
2882
2902
|
}
|
|
2903
|
+
const mentionByDomain = new Map(mentionLandscape.competitors.map((m) => [m.domain, m]));
|
|
2883
2904
|
const rows = competitors2.map((c) => {
|
|
2884
2905
|
const tone = pressureTone(c.pressureLabel);
|
|
2906
|
+
const mention = mentionByDomain.get(c.domain);
|
|
2907
|
+
const mentionCount = mention?.mentionCount ?? 0;
|
|
2908
|
+
const mentionTotal = mention?.totalCount ?? mentionLandscape.totalAnswerSnapshots;
|
|
2885
2909
|
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length} cited URL${c.theirCitedPages.length > 1 ? "s" : ""}</summary>
|
|
2886
2910
|
<ul>${c.theirCitedPages.map((p) => `<li><a href="${escapeHtml(p.url)}">${escapeHtml(p.url)}</a> <span class="cited-for">${escapeHtml(p.citedFor.join(", "))}</span></li>`).join("")}</ul>
|
|
2887
2911
|
</details>` : "";
|
|
@@ -2889,17 +2913,26 @@ function renderCompetitorLandscape(report) {
|
|
|
2889
2913
|
<td>${escapeHtml(c.domain)}</td>
|
|
2890
2914
|
<td><span class="badge tone-${tone}">${escapeHtml(c.pressureLabel)}</span></td>
|
|
2891
2915
|
<td class="numeric">${c.citationCount} / ${c.totalCount}</td>
|
|
2916
|
+
<td class="numeric">${mentionCount} / ${mentionTotal}</td>
|
|
2892
2917
|
<td class="numeric">${c.sharePct}%</td>
|
|
2893
2918
|
<td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}${pagesDisclosure}</td>
|
|
2894
2919
|
</tr>`;
|
|
2895
2920
|
}).join("");
|
|
2896
2921
|
const table = competitors2.length > 0 ? `<table class="report-table">
|
|
2897
|
-
<thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
|
|
2922
|
+
<thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">Mentions</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
|
|
2898
2923
|
<tbody>${rows}</tbody>
|
|
2899
2924
|
</table>` : renderEmpty("No competitors configured.");
|
|
2925
|
+
const citationBars = renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain);
|
|
2926
|
+
const mentionBars = renderMentionBars(mentionLandscape, report.meta.project.canonicalDomain);
|
|
2927
|
+
const charts = citationBars && mentionBars ? `<div class="chart-grid">${citationBars}${mentionBars}</div>` : `${citationBars}${mentionBars}`;
|
|
2900
2928
|
return section(
|
|
2901
|
-
{
|
|
2902
|
-
|
|
2929
|
+
{
|
|
2930
|
+
id: "competitor-landscape",
|
|
2931
|
+
eyebrow: "Section 3",
|
|
2932
|
+
title: "Competitor Landscape",
|
|
2933
|
+
intro: "Where tracked competitors appear in AI answers compared to your domain \u2014 both in source citations and in the answer text itself."
|
|
2934
|
+
},
|
|
2935
|
+
`${charts}${table}`
|
|
2903
2936
|
);
|
|
2904
2937
|
}
|
|
2905
2938
|
function renderDonut(buckets) {
|
|
@@ -2946,7 +2979,7 @@ function renderAiSourceOrigin(report) {
|
|
|
2946
2979
|
const origin = report.aiSourceOrigin;
|
|
2947
2980
|
if (origin.categories.length === 0 && origin.topDomains.length === 0) {
|
|
2948
2981
|
return section(
|
|
2949
|
-
{ id: "ai-source-origin", eyebrow: "Section 4", title: "AI
|
|
2982
|
+
{ id: "ai-source-origin", eyebrow: "Section 4", title: "AI Citation Sources" },
|
|
2950
2983
|
renderEmpty("No source data yet. Run a visibility sweep first.")
|
|
2951
2984
|
);
|
|
2952
2985
|
}
|
|
@@ -2961,7 +2994,12 @@ function renderAiSourceOrigin(report) {
|
|
|
2961
2994
|
<tbody>${rows}</tbody>
|
|
2962
2995
|
</table>` : "";
|
|
2963
2996
|
return section(
|
|
2964
|
-
{
|
|
2997
|
+
{
|
|
2998
|
+
id: "ai-source-origin",
|
|
2999
|
+
eyebrow: "Section 4",
|
|
3000
|
+
title: "AI Citation Sources",
|
|
3001
|
+
intro: "Every external website AI engines cited as a source for your tracked keywords in the latest sweep \u2014 categorized by site type (Reddit, YouTube, news, etc.) on the left and ranked by citation count on the right. Your own domains are excluded; tracked competitors are flagged."
|
|
3002
|
+
},
|
|
2965
3003
|
`${renderDonut(origin.categories)}${table}`
|
|
2966
3004
|
);
|
|
2967
3005
|
}
|
|
@@ -3040,7 +3078,7 @@ function renderGsc(report) {
|
|
|
3040
3078
|
</div>`);
|
|
3041
3079
|
}
|
|
3042
3080
|
return section(
|
|
3043
|
-
{ id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "
|
|
3081
|
+
{ id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Your site\u2019s performance in Google\u2019s regular (non-AI) search results \u2014 top queries that drove impressions, intent breakdown, and the click trend, sourced from Google Search Console for the most recent sync window." },
|
|
3044
3082
|
`<div class="metric-grid">
|
|
3045
3083
|
<div class="metric"><div class="label">Total clicks</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
|
|
3046
3084
|
<div class="metric"><div class="label">Total impressions</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
|
|
@@ -3085,7 +3123,7 @@ function renderGa(report) {
|
|
|
3085
3123
|
<td class="numeric">${c.sharePct}%</td>
|
|
3086
3124
|
</tr>`).join("");
|
|
3087
3125
|
return section(
|
|
3088
|
-
{ id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `
|
|
3126
|
+
{ id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Total sessions and users on your site between ${formatDate(ga.periodStart)} and ${formatDate(ga.periodEnd)}, with the top landing pages and channel breakdown \u2014 sourced from Google Analytics 4.` },
|
|
3089
3127
|
`<div class="metric-grid">
|
|
3090
3128
|
<div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
|
|
3091
3129
|
<div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
|
|
@@ -3126,7 +3164,7 @@ function renderSocial(report) {
|
|
|
3126
3164
|
<td class="numeric">${formatNumber(c.sessions)}</td>
|
|
3127
3165
|
</tr>`).join("");
|
|
3128
3166
|
return section(
|
|
3129
|
-
{ id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "
|
|
3167
|
+
{ id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Sessions on your site sent by social platforms (LinkedIn, Facebook, X, etc.) \u2014 paid versus organic split and the top campaigns that drove them. Sourced from Google Analytics 4." },
|
|
3130
3168
|
`<div class="metric-grid">
|
|
3131
3169
|
<div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
|
|
3132
3170
|
<div class="metric"><div class="label">Organic social</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
|
|
@@ -3173,7 +3211,7 @@ function renderAiReferrals(report) {
|
|
|
3173
3211
|
"AI referral sessions over time"
|
|
3174
3212
|
);
|
|
3175
3213
|
return section(
|
|
3176
|
-
{ id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Sessions
|
|
3214
|
+
{ id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Sessions on your site referred by AI answer engines (ChatGPT, Perplexity, Claude, Copilot, Gemini, etc.) \u2014 broken down by referrer with a daily trend and the top landing pages. Sourced from Google Analytics 4." },
|
|
3177
3215
|
`<div class="metric-grid">
|
|
3178
3216
|
<div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
|
|
3179
3217
|
<div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
|
|
@@ -3219,7 +3257,7 @@ function renderIndexingHealth(report) {
|
|
|
3219
3257
|
}).join("");
|
|
3220
3258
|
const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
|
|
3221
3259
|
return section(
|
|
3222
|
-
{ id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `
|
|
3260
|
+
{ id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `What share of your tracked URLs are currently indexed in ${ih.provider === "google" ? "Google" : "Bing"} \u2014 sourced from ${ih.provider === "google" ? "Google Search Console URL Inspection" : "Bing Webmaster Tools URL Inspection"}. Pages absent from the index can\u2019t be retrieved by AI engines either.` },
|
|
3223
3261
|
`<div class="metric-grid">
|
|
3224
3262
|
<div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
|
|
3225
3263
|
<div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
|
|
@@ -3259,7 +3297,7 @@ function renderCitationsTrend(report) {
|
|
|
3259
3297
|
<td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
|
|
3260
3298
|
</tr>`).join("");
|
|
3261
3299
|
return section(
|
|
3262
|
-
{ id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "
|
|
3300
|
+
{ id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Citation rate across every visibility sweep \u2014 the share of (keyword \xD7 provider) pairs in each run where your domain appeared in the source list, with a per-provider breakdown beneath." },
|
|
3263
3301
|
`${chart}
|
|
3264
3302
|
<div class="chart-card"><h3>Run-by-run breakdown</h3>
|
|
3265
3303
|
<table class="report-table">
|
|
@@ -3290,7 +3328,7 @@ function renderInsights(report) {
|
|
|
3290
3328
|
</tr>`;
|
|
3291
3329
|
}).join("");
|
|
3292
3330
|
return section(
|
|
3293
|
-
{ id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "
|
|
3331
|
+
{ id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Regressions (citations lost), gains (citations won), and opportunities surfaced by the intelligence engine across the most recent sweeps \u2014 ordered by severity and recurrence." },
|
|
3294
3332
|
`<table class="report-table">
|
|
3295
3333
|
<thead><tr><th>Severity</th><th>Title</th><th>Keyword</th><th>Provider</th><th>Recommendation</th></tr></thead>
|
|
3296
3334
|
<tbody>${rows}</tbody>
|
|
@@ -3300,8 +3338,9 @@ function renderInsights(report) {
|
|
|
3300
3338
|
function renderOpportunities(report) {
|
|
3301
3339
|
const opps = report.contentOpportunities;
|
|
3302
3340
|
if (opps.length === 0) return "";
|
|
3341
|
+
const canonical = report.meta.project.canonicalDomain;
|
|
3303
3342
|
const rows = opps.slice(0, 10).map((o) => {
|
|
3304
|
-
const ourPage = o.ourBestPage ? `<a href="${escapeHtml(o.ourBestPage.url)}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
3343
|
+
const ourPage = o.ourBestPage ? `<a href="${escapeHtml(absolutizeProjectUrl(o.ourBestPage.url, canonical))}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
3305
3344
|
const winning = o.winningCompetitor ? `<a href="${escapeHtml(o.winningCompetitor.url)}">${escapeHtml(o.winningCompetitor.domain)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
3306
3345
|
return `<tr>
|
|
3307
3346
|
<td>${escapeHtml(o.query)}</td>
|
|
@@ -3318,7 +3357,7 @@ function renderOpportunities(report) {
|
|
|
3318
3357
|
id: "content-opportunities",
|
|
3319
3358
|
eyebrow: "Section 12",
|
|
3320
3359
|
title: "Content Opportunities",
|
|
3321
|
-
intro: "
|
|
3360
|
+
intro: "Queries where you have search demand or competitor citation pressure but aren\u2019t winning AI citations. Each row carries a suggested action (create / refresh / expand / add-schema). Top 10 shown."
|
|
3322
3361
|
},
|
|
3323
3362
|
`<table class="report-table">
|
|
3324
3363
|
<thead><tr><th>Query</th><th>Action</th><th class="numeric">Score</th><th>Our page</th><th>Winning competitor</th><th>Demand</th><th>Confidence</th></tr></thead>
|
|
@@ -3330,7 +3369,7 @@ function renderRecommendedNextSteps(report) {
|
|
|
3330
3369
|
const steps = report.recommendedNextSteps;
|
|
3331
3370
|
if (steps.length === 0) {
|
|
3332
3371
|
return section(
|
|
3333
|
-
{ id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
|
|
3372
|
+
{ id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps", intro: "Action items bucketed by horizon (immediate, short-term, medium-term), drawn from open insights and the highest-ranked content opportunities." },
|
|
3334
3373
|
renderEmpty("No outstanding actions.")
|
|
3335
3374
|
);
|
|
3336
3375
|
}
|
|
@@ -3341,7 +3380,7 @@ function renderRecommendedNextSteps(report) {
|
|
|
3341
3380
|
<span class="rationale">${escapeHtml(s.rationale)}</span>
|
|
3342
3381
|
</div>`).join("");
|
|
3343
3382
|
return section(
|
|
3344
|
-
{ id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
|
|
3383
|
+
{ id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps", intro: "Action items bucketed by horizon (immediate, short-term, medium-term), drawn from open insights and the highest-ranked content opportunities." },
|
|
3345
3384
|
`<div class="steps">${items}</div>`
|
|
3346
3385
|
);
|
|
3347
3386
|
}
|
|
@@ -3817,6 +3856,56 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
|
|
|
3817
3856
|
competitorRows.sort((a, b) => b.citationCount - a.citationCount);
|
|
3818
3857
|
return { projectCitationCount, competitors: competitorRows };
|
|
3819
3858
|
}
|
|
3859
|
+
function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName, projectDomains, keywordLookup) {
|
|
3860
|
+
let projectMentionCount = 0;
|
|
3861
|
+
let totalAnswerSnapshots = 0;
|
|
3862
|
+
const competitorMap = /* @__PURE__ */ new Map();
|
|
3863
|
+
for (const c of competitorDomains) {
|
|
3864
|
+
competitorMap.set(c, { count: 0, keywords: /* @__PURE__ */ new Set() });
|
|
3865
|
+
}
|
|
3866
|
+
for (const snap of snapshots) {
|
|
3867
|
+
const text = snap.answerText;
|
|
3868
|
+
if (!text) continue;
|
|
3869
|
+
totalAnswerSnapshots++;
|
|
3870
|
+
const kw = keywordLookup.byId.get(snap.keywordId);
|
|
3871
|
+
const projectMentioned = snap.answerMentioned ?? determineAnswerMentioned(
|
|
3872
|
+
text,
|
|
3873
|
+
projectDisplayName,
|
|
3874
|
+
projectDomains
|
|
3875
|
+
);
|
|
3876
|
+
if (projectMentioned) projectMentionCount++;
|
|
3877
|
+
for (const competitor of competitorDomains) {
|
|
3878
|
+
const brand = brandLabelFromDomain(competitor);
|
|
3879
|
+
const mentioned = determineAnswerMentioned(text, brand, [competitor]);
|
|
3880
|
+
if (mentioned) {
|
|
3881
|
+
const entry = competitorMap.get(competitor);
|
|
3882
|
+
entry.count++;
|
|
3883
|
+
if (kw) entry.keywords.add(kw);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
const totalMentionedSlots = projectMentionCount + [...competitorMap.values()].reduce((sum, v) => sum + v.count, 0);
|
|
3888
|
+
const competitorRows = [...competitorMap.entries()].map(([domain, data]) => {
|
|
3889
|
+
const ratio = totalAnswerSnapshots > 0 ? data.count / totalAnswerSnapshots : 0;
|
|
3890
|
+
let pressureLabel = "None";
|
|
3891
|
+
if (data.count > 0) {
|
|
3892
|
+
if (ratio >= 0.5) pressureLabel = "High";
|
|
3893
|
+
else if (ratio >= 0.2) pressureLabel = "Moderate";
|
|
3894
|
+
else pressureLabel = "Low";
|
|
3895
|
+
}
|
|
3896
|
+
const sharePct = totalMentionedSlots > 0 ? Math.round(data.count / totalMentionedSlots * 100) : 0;
|
|
3897
|
+
return {
|
|
3898
|
+
domain,
|
|
3899
|
+
mentionCount: data.count,
|
|
3900
|
+
totalCount: totalAnswerSnapshots,
|
|
3901
|
+
pressureLabel,
|
|
3902
|
+
mentionedKeywords: [...data.keywords].sort(),
|
|
3903
|
+
sharePct
|
|
3904
|
+
};
|
|
3905
|
+
});
|
|
3906
|
+
competitorRows.sort((a, b) => b.mentionCount - a.mentionCount);
|
|
3907
|
+
return { projectMentionCount, totalAnswerSnapshots, competitors: competitorRows };
|
|
3908
|
+
}
|
|
3820
3909
|
function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains) {
|
|
3821
3910
|
const categoryCounts = /* @__PURE__ */ new Map();
|
|
3822
3911
|
const domainCounts = /* @__PURE__ */ new Map();
|
|
@@ -4261,6 +4350,13 @@ function buildProjectReport(db, projectName) {
|
|
|
4261
4350
|
projectDomains,
|
|
4262
4351
|
keywordLookup
|
|
4263
4352
|
);
|
|
4353
|
+
const mentionLandscape = buildMentionLandscape(
|
|
4354
|
+
latestSnapshots,
|
|
4355
|
+
competitorDomains,
|
|
4356
|
+
project.displayName,
|
|
4357
|
+
projectDomains,
|
|
4358
|
+
keywordLookup
|
|
4359
|
+
);
|
|
4264
4360
|
const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
|
|
4265
4361
|
const trackedKeywords = [...keywordLookup.byId.values()];
|
|
4266
4362
|
const gscSection = buildGscSection(
|
|
@@ -4353,6 +4449,7 @@ function buildProjectReport(db, projectName) {
|
|
|
4353
4449
|
},
|
|
4354
4450
|
citationScorecard,
|
|
4355
4451
|
competitorLandscape,
|
|
4452
|
+
mentionLandscape,
|
|
4356
4453
|
aiSourceOrigin,
|
|
4357
4454
|
gsc: gscSection,
|
|
4358
4455
|
ga: gaSection,
|
|
@@ -7131,7 +7228,7 @@ var routeCatalog = [
|
|
|
7131
7228
|
path: "/api/v1/projects/{name}/report",
|
|
7132
7229
|
summary: "Aggregated client-facing AEO report",
|
|
7133
7230
|
tags: ["report"],
|
|
7134
|
-
description: "Bundles every section the canonry-report HTML output needs (executive summary, citation scorecard, competitor landscape, AI
|
|
7231
|
+
description: "Bundles every section the canonry-report HTML output needs (executive summary, citation scorecard, competitor landscape \u2014 citation + mention landscapes, AI citation sources, GSC, GA4, social/AI referrals, indexing health, citations trend, insights, and recommended next steps) into a single JSON payload. Backs `canonry report <project>`.",
|
|
7135
7232
|
parameters: [nameParameter],
|
|
7136
7233
|
responses: {
|
|
7137
7234
|
200: { description: "Report returned." },
|
|
@@ -8823,6 +8920,66 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
|
8823
8920
|
ga4Log("info", "fetch-aggregate.done", { propertyId, ...summary });
|
|
8824
8921
|
return summary;
|
|
8825
8922
|
}
|
|
8923
|
+
var WINDOW_DAYS = { "7d": 7, "30d": 30, "90d": 90 };
|
|
8924
|
+
async function fetchWindowSummary(accessToken, propertyId, windowKey) {
|
|
8925
|
+
validateAccessToken2(accessToken);
|
|
8926
|
+
validatePropertyId(propertyId);
|
|
8927
|
+
const days = WINDOW_DAYS[windowKey];
|
|
8928
|
+
if (!days) {
|
|
8929
|
+
throw new GA4ApiError(`Unsupported windowKey "${windowKey}" \u2014 must be 7d, 30d, or 90d`, 400);
|
|
8930
|
+
}
|
|
8931
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
8932
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
8933
|
+
startDate.setDate(startDate.getDate() - days);
|
|
8934
|
+
const dateRange = { startDate: formatDate2(startDate), endDate: formatDate2(endDate) };
|
|
8935
|
+
ga4Log("info", "fetch-window-summary.start", { propertyId, windowKey, days });
|
|
8936
|
+
const batchRes = await batchRunReports(accessToken, propertyId, [
|
|
8937
|
+
{
|
|
8938
|
+
dateRanges: [dateRange],
|
|
8939
|
+
dimensions: [],
|
|
8940
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
8941
|
+
limit: 1
|
|
8942
|
+
},
|
|
8943
|
+
{
|
|
8944
|
+
dateRanges: [dateRange],
|
|
8945
|
+
dimensions: [],
|
|
8946
|
+
metrics: [{ name: "sessions" }],
|
|
8947
|
+
dimensionFilter: {
|
|
8948
|
+
filter: {
|
|
8949
|
+
fieldName: "sessionDefaultChannelGrouping",
|
|
8950
|
+
stringFilter: { matchType: "EXACT", value: "Organic Search" }
|
|
8951
|
+
}
|
|
8952
|
+
},
|
|
8953
|
+
limit: 1
|
|
8954
|
+
},
|
|
8955
|
+
{
|
|
8956
|
+
dateRanges: [dateRange],
|
|
8957
|
+
dimensions: [],
|
|
8958
|
+
metrics: [{ name: "sessions" }],
|
|
8959
|
+
dimensionFilter: {
|
|
8960
|
+
filter: {
|
|
8961
|
+
fieldName: "sessionDefaultChannelGrouping",
|
|
8962
|
+
stringFilter: { matchType: "EXACT", value: "Direct" }
|
|
8963
|
+
}
|
|
8964
|
+
},
|
|
8965
|
+
limit: 1
|
|
8966
|
+
}
|
|
8967
|
+
]);
|
|
8968
|
+
const totalRow = batchRes[0]?.rows?.[0];
|
|
8969
|
+
const organicRow = batchRes[1]?.rows?.[0];
|
|
8970
|
+
const directRow = batchRes[2]?.rows?.[0];
|
|
8971
|
+
const summary = {
|
|
8972
|
+
windowKey,
|
|
8973
|
+
periodStart: formatDate2(startDate),
|
|
8974
|
+
periodEnd: formatDate2(endDate),
|
|
8975
|
+
totalSessions: parseInt(totalRow?.metricValues[0]?.value ?? "0", 10) || 0,
|
|
8976
|
+
totalUsers: parseInt(totalRow?.metricValues[1]?.value ?? "0", 10) || 0,
|
|
8977
|
+
totalOrganicSessions: parseInt(organicRow?.metricValues[0]?.value ?? "0", 10) || 0,
|
|
8978
|
+
totalDirectSessions: parseInt(directRow?.metricValues[0]?.value ?? "0", 10) || 0
|
|
8979
|
+
};
|
|
8980
|
+
ga4Log("info", "fetch-window-summary.done", { propertyId, ...summary });
|
|
8981
|
+
return summary;
|
|
8982
|
+
}
|
|
8826
8983
|
async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
8827
8984
|
validateAccessToken2(accessToken);
|
|
8828
8985
|
validatePropertyId(propertyId);
|
|
@@ -10695,13 +10852,18 @@ async function ga4Routes(app, opts) {
|
|
|
10695
10852
|
let rows = [];
|
|
10696
10853
|
let aiReferrals = [];
|
|
10697
10854
|
let socialReferrals = [];
|
|
10698
|
-
const
|
|
10855
|
+
const WINDOW_KEYS = ["7d", "30d", "90d"];
|
|
10856
|
+
const fetches = [
|
|
10857
|
+
fetchAggregateSummary(accessToken, propertyId, days),
|
|
10858
|
+
...WINDOW_KEYS.map((w) => fetchWindowSummary(accessToken, propertyId, w))
|
|
10859
|
+
];
|
|
10699
10860
|
if (syncTraffic) fetches.push(fetchTrafficByLandingPage(accessToken, propertyId, days));
|
|
10700
10861
|
if (syncAi) fetches.push(fetchAiReferrals(accessToken, propertyId, days));
|
|
10701
10862
|
if (syncSocial) fetches.push(fetchSocialReferrals(accessToken, propertyId, days));
|
|
10702
10863
|
const results = await Promise.all(fetches);
|
|
10703
10864
|
const summary = results[0];
|
|
10704
|
-
|
|
10865
|
+
const windowSummaries = results.slice(1, 1 + WINDOW_KEYS.length);
|
|
10866
|
+
let idx = 1 + WINDOW_KEYS.length;
|
|
10705
10867
|
if (syncTraffic) {
|
|
10706
10868
|
rows = results[idx++];
|
|
10707
10869
|
}
|
|
@@ -10798,6 +10960,22 @@ async function ga4Routes(app, opts) {
|
|
|
10798
10960
|
syncedAt: now,
|
|
10799
10961
|
syncRunId: runId
|
|
10800
10962
|
}).run();
|
|
10963
|
+
tx.delete(gaTrafficWindowSummaries).where(eq21(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
10964
|
+
for (const ws of windowSummaries) {
|
|
10965
|
+
tx.insert(gaTrafficWindowSummaries).values({
|
|
10966
|
+
id: crypto16.randomUUID(),
|
|
10967
|
+
projectId: project.id,
|
|
10968
|
+
windowKey: ws.windowKey,
|
|
10969
|
+
periodStart: ws.periodStart,
|
|
10970
|
+
periodEnd: ws.periodEnd,
|
|
10971
|
+
totalSessions: ws.totalSessions,
|
|
10972
|
+
totalOrganicSessions: ws.totalOrganicSessions,
|
|
10973
|
+
totalDirectSessions: ws.totalDirectSessions,
|
|
10974
|
+
totalUsers: ws.totalUsers,
|
|
10975
|
+
syncedAt: now,
|
|
10976
|
+
syncRunId: runId
|
|
10977
|
+
}).run();
|
|
10978
|
+
}
|
|
10801
10979
|
}
|
|
10802
10980
|
});
|
|
10803
10981
|
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq21(runs.id, runId)).run();
|
|
@@ -10846,16 +11024,28 @@ async function ga4Routes(app, opts) {
|
|
|
10846
11024
|
if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
10847
11025
|
const socialConditions = [eq21(gaSocialReferrals.projectId, project.id)];
|
|
10848
11026
|
if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
10849
|
-
const
|
|
11027
|
+
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
11028
|
+
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
11029
|
+
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
11030
|
+
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
11031
|
+
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
11032
|
+
}).from(gaTrafficWindowSummaries).where(
|
|
11033
|
+
and9(
|
|
11034
|
+
eq21(gaTrafficWindowSummaries.projectId, project.id),
|
|
11035
|
+
eq21(gaTrafficWindowSummaries.windowKey, window)
|
|
11036
|
+
)
|
|
11037
|
+
).get() : null;
|
|
11038
|
+
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
10850
11039
|
totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
10851
11040
|
totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
10852
11041
|
totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
10853
|
-
}).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get() :
|
|
11042
|
+
}).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get() : null;
|
|
11043
|
+
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
10854
11044
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
10855
11045
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
10856
11046
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
10857
11047
|
}).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
|
|
10858
|
-
const directTotalRow = app.db.select({
|
|
11048
|
+
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
10859
11049
|
totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
10860
11050
|
}).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get();
|
|
10861
11051
|
const summaryMeta = app.db.select({
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
projectUpsertRequestSchema,
|
|
11
11
|
runTriggerRequestSchema,
|
|
12
12
|
scheduleUpsertRequestSchema
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-RDX6GBWM.js";
|
|
14
14
|
|
|
15
15
|
// src/config.ts
|
|
16
16
|
import fs from "fs";
|
|
@@ -1135,7 +1135,7 @@ var canonryMcpTools = [
|
|
|
1135
1135
|
defineTool({
|
|
1136
1136
|
name: "canonry_report",
|
|
1137
1137
|
title: "Get aggregated AEO report",
|
|
1138
|
-
description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-keyword \xD7 per-provider citation matrix, competitor landscape, AI
|
|
1138
|
+
description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-keyword \xD7 per-provider citation matrix, competitor landscape, AI citation sources, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render the self-contained HTML.",
|
|
1139
1139
|
access: "read",
|
|
1140
1140
|
tier: "monitoring",
|
|
1141
1141
|
inputSchema: projectInputSchema,
|
|
@@ -1711,6 +1711,17 @@ function dropTrailingSlash(path) {
|
|
|
1711
1711
|
}
|
|
1712
1712
|
return path;
|
|
1713
1713
|
}
|
|
1714
|
+
function absolutizeProjectUrl(url, canonicalDomain) {
|
|
1715
|
+
if (!url) return "";
|
|
1716
|
+
const trimmed = url.trim();
|
|
1717
|
+
if (!trimmed) return "";
|
|
1718
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
1719
|
+
if (trimmed.startsWith("//")) return `https:${trimmed}`;
|
|
1720
|
+
const host = canonicalDomain.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
1721
|
+
if (!host) return trimmed;
|
|
1722
|
+
if (trimmed.startsWith("/")) return `https://${host}${trimmed}`;
|
|
1723
|
+
return `https://${host}/${trimmed}`;
|
|
1724
|
+
}
|
|
1714
1725
|
function normalizeUrlPath(input) {
|
|
1715
1726
|
if (input == null) return null;
|
|
1716
1727
|
let trimmed = input.trim();
|
|
@@ -1910,6 +1921,7 @@ export {
|
|
|
1910
1921
|
CheckScopes,
|
|
1911
1922
|
CheckCategories,
|
|
1912
1923
|
summarizeCheckResults,
|
|
1924
|
+
absolutizeProjectUrl,
|
|
1913
1925
|
normalizeUrlPath,
|
|
1914
1926
|
emptyCitationVisibility,
|
|
1915
1927
|
citationStateToCited,
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ContentActions,
|
|
3
3
|
RunKinds,
|
|
4
4
|
__export
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-RDX6GBWM.js";
|
|
6
6
|
|
|
7
7
|
// src/intelligence-service.ts
|
|
8
8
|
import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
|
|
@@ -33,6 +33,7 @@ __export(schema_exports, {
|
|
|
33
33
|
gaSocialReferrals: () => gaSocialReferrals,
|
|
34
34
|
gaTrafficSnapshots: () => gaTrafficSnapshots,
|
|
35
35
|
gaTrafficSummaries: () => gaTrafficSummaries,
|
|
36
|
+
gaTrafficWindowSummaries: () => gaTrafficWindowSummaries,
|
|
36
37
|
googleConnections: () => googleConnections,
|
|
37
38
|
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
38
39
|
gscSearchData: () => gscSearchData,
|
|
@@ -392,6 +393,23 @@ var gaTrafficSummaries = sqliteTable("ga_traffic_summaries", {
|
|
|
392
393
|
index("idx_ga_summary_project").on(table.projectId),
|
|
393
394
|
index("idx_ga_summary_run").on(table.syncRunId)
|
|
394
395
|
]);
|
|
396
|
+
var gaTrafficWindowSummaries = sqliteTable("ga_traffic_window_summaries", {
|
|
397
|
+
id: text("id").primaryKey(),
|
|
398
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
399
|
+
/** '7d' | '30d' | '90d' */
|
|
400
|
+
windowKey: text("window_key").notNull(),
|
|
401
|
+
periodStart: text("period_start").notNull(),
|
|
402
|
+
periodEnd: text("period_end").notNull(),
|
|
403
|
+
totalSessions: integer("total_sessions").notNull().default(0),
|
|
404
|
+
totalOrganicSessions: integer("total_organic_sessions").notNull().default(0),
|
|
405
|
+
totalDirectSessions: integer("total_direct_sessions").notNull().default(0),
|
|
406
|
+
totalUsers: integer("total_users").notNull().default(0),
|
|
407
|
+
syncedAt: text("synced_at").notNull(),
|
|
408
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" })
|
|
409
|
+
}, (table) => [
|
|
410
|
+
uniqueIndex("idx_ga_window_summary_unique").on(table.projectId, table.windowKey),
|
|
411
|
+
index("idx_ga_window_summary_run").on(table.syncRunId)
|
|
412
|
+
]);
|
|
395
413
|
var usageCounters = sqliteTable("usage_counters", {
|
|
396
414
|
id: text("id").primaryKey(),
|
|
397
415
|
scope: text("scope").notNull(),
|
|
@@ -1332,6 +1350,29 @@ var MIGRATION_VERSIONS = [
|
|
|
1332
1350
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v3
|
|
1333
1351
|
ON ga_ai_referrals(project_id, date, source, medium, source_dimension, landing_page)`
|
|
1334
1352
|
]
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
version: 47,
|
|
1356
|
+
name: "ga-traffic-window-summaries",
|
|
1357
|
+
statements: [
|
|
1358
|
+
`CREATE TABLE IF NOT EXISTS ga_traffic_window_summaries (
|
|
1359
|
+
id TEXT PRIMARY KEY,
|
|
1360
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1361
|
+
window_key TEXT NOT NULL,
|
|
1362
|
+
period_start TEXT NOT NULL,
|
|
1363
|
+
period_end TEXT NOT NULL,
|
|
1364
|
+
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
1365
|
+
total_organic_sessions INTEGER NOT NULL DEFAULT 0,
|
|
1366
|
+
total_direct_sessions INTEGER NOT NULL DEFAULT 0,
|
|
1367
|
+
total_users INTEGER NOT NULL DEFAULT 0,
|
|
1368
|
+
synced_at TEXT NOT NULL,
|
|
1369
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE
|
|
1370
|
+
)`,
|
|
1371
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_window_summary_unique
|
|
1372
|
+
ON ga_traffic_window_summaries(project_id, window_key)`,
|
|
1373
|
+
`CREATE INDEX IF NOT EXISTS idx_ga_window_summary_run
|
|
1374
|
+
ON ga_traffic_window_summaries(sync_run_id)`
|
|
1375
|
+
]
|
|
1335
1376
|
}
|
|
1336
1377
|
];
|
|
1337
1378
|
function isDuplicateColumnError(err) {
|
|
@@ -2348,6 +2389,7 @@ export {
|
|
|
2348
2389
|
gaAiReferrals,
|
|
2349
2390
|
gaSocialReferrals,
|
|
2350
2391
|
gaTrafficSummaries,
|
|
2392
|
+
gaTrafficWindowSummaries,
|
|
2351
2393
|
usageCounters,
|
|
2352
2394
|
insights,
|
|
2353
2395
|
healthSnapshots,
|