@ainyc/canonry 4.95.0 → 4.97.0
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/canonry/references/canonry-cli.md +1 -0
- package/assets/assets/{BacklinksPage-CT3DfxMw.js → BacklinksPage-BI9BtHjp.js} +1 -1
- package/assets/assets/{ChartPrimitives-B6Dleq3z.js → ChartPrimitives-BcjGunGr.js} +1 -1
- package/assets/assets/ProjectPage-BdoberYt.js +6 -0
- package/assets/assets/{RunRow-C7rEdPSI.js → RunRow-BT2kpdQR.js} +1 -1
- package/assets/assets/{RunsPage-ByohAHOY.js → RunsPage-BAgZuX7j.js} +1 -1
- package/assets/assets/{SettingsPage-D4dwPwZy.js → SettingsPage-BvKcpivM.js} +1 -1
- package/assets/assets/{TrafficPage-Kh1CSxMr.js → TrafficPage-BdGOZv1e.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CIlOFRYB.js → TrafficSourceDetailPage-C_N2D9xy.js} +1 -1
- package/assets/assets/{arrow-left-BoX1Vp7B.js → arrow-left-C8Fbup8X.js} +1 -1
- package/assets/assets/{extract-error-message-ZqsMkFg8.js → extract-error-message-C29K35Gm.js} +1 -1
- package/assets/assets/index-BGfyt3LX.js +210 -0
- package/assets/assets/{index-DiykVUPr.css → index-BsZNCwg8.css} +1 -1
- package/assets/assets/{trash-2-A1PQNK3E.js → trash-2-DWLIELE_.js} +1 -1
- package/assets/assets/{vendor-markdown-DK7fbRNb.js → vendor-markdown-6dwjPOKK.js} +1 -1
- package/assets/assets/{vendor-radix-B57xfQbP.js → vendor-radix-CQ77EO2y.js} +1 -1
- package/assets/assets/{vendor-recharts-ClRVR6aX.js → vendor-recharts-C9EZkgbP.js} +1 -1
- package/assets/assets/vendor-tanstack-Be8JdV5M.js +1 -0
- package/assets/index.html +6 -6
- package/dist/{chunk-BUGJOQNW.js → chunk-E2FDXNMW.js} +82 -7
- package/dist/{chunk-4E4JPS37.js → chunk-NUGGUQH3.js} +13 -7
- package/dist/{chunk-FFSJRZJR.js → chunk-SRJ5IGNK.js} +14 -6
- package/dist/{chunk-7YU6ILWL.js → chunk-TXNKKTFD.js} +99 -65
- package/dist/cli.js +19 -6
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-PGEGDNY6.js → intelligence-service-DC2TD7RR.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +9 -9
- package/assets/assets/ProjectPage-DeEE66hg.js +0 -6
- package/assets/assets/index-k1XIw8WK.js +0 -210
- package/assets/assets/vendor-tanstack-Dq7p98wZ.js +0 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
projectUpsertRequestSchema,
|
|
17
17
|
queryBatchRequestSchema,
|
|
18
18
|
queryGenerateRequestSchema,
|
|
19
|
+
reportPeriodSchema,
|
|
19
20
|
runTriggerRequestSchema,
|
|
20
21
|
schedulableRunKindSchema,
|
|
21
22
|
scheduleUpsertRequestSchema,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
trafficConnectVercelRequestSchema,
|
|
24
25
|
trafficConnectWordpressRequestSchema,
|
|
25
26
|
trafficEventKindSchema
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-E2FDXNMW.js";
|
|
27
28
|
|
|
28
29
|
// src/config.ts
|
|
29
30
|
import fs from "fs";
|
|
@@ -3899,9 +3900,13 @@ var ApiClient = class {
|
|
|
3899
3900
|
})
|
|
3900
3901
|
);
|
|
3901
3902
|
}
|
|
3902
|
-
async getReport(project) {
|
|
3903
|
+
async getReport(project, opts) {
|
|
3903
3904
|
return this.invoke(
|
|
3904
|
-
() => getApiV1ProjectsByNameReport({
|
|
3905
|
+
() => getApiV1ProjectsByNameReport({
|
|
3906
|
+
client: this.heyClient,
|
|
3907
|
+
path: { name: project },
|
|
3908
|
+
...opts?.period !== void 0 && { query: { period: opts.period } }
|
|
3909
|
+
})
|
|
3905
3910
|
);
|
|
3906
3911
|
}
|
|
3907
3912
|
// ── Queries / keywords / competitors ────────────────────────────────────
|
|
@@ -5639,13 +5644,16 @@ var canonryMcpTools = [
|
|
|
5639
5644
|
defineTool({
|
|
5640
5645
|
name: "canonry_report",
|
|
5641
5646
|
title: "Get aggregated AEO report",
|
|
5642
|
-
description: "Returns the full canonical AEO report bundle for a project \u2014 executive summary, client summary, agency diagnostics, action plan, per-query \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 audience-specific HTML.",
|
|
5647
|
+
description: "Returns the full canonical AEO report bundle for a project \u2014 executive summary, client summary, agency diagnostics, action plan, per-query \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 audience-specific HTML. Pass `period` (7/14/30/90 days, default 30) to scope the GSC/GA4/server-activity sections and the period-over-period comparisons.",
|
|
5643
5648
|
access: "read",
|
|
5644
5649
|
tier: "monitoring",
|
|
5645
|
-
inputSchema:
|
|
5650
|
+
inputSchema: z2.object({
|
|
5651
|
+
project: projectNameSchema,
|
|
5652
|
+
period: reportPeriodSchema.optional()
|
|
5653
|
+
}),
|
|
5646
5654
|
annotations: readAnnotations(),
|
|
5647
5655
|
openApiOperations: ["GET /api/v1/projects/{name}/report"],
|
|
5648
|
-
handler: (client2, input) => client2.getReport(input.project)
|
|
5656
|
+
handler: (client2, input) => client2.getReport(input.project, input.period !== void 0 ? { period: input.period } : void 0)
|
|
5649
5657
|
}),
|
|
5650
5658
|
defineTool({
|
|
5651
5659
|
name: "canonry_analytics_metrics",
|
|
@@ -124,12 +124,14 @@ import {
|
|
|
124
124
|
extractAnswerMentions,
|
|
125
125
|
findDuplicateLocationLabels,
|
|
126
126
|
forbidden,
|
|
127
|
+
formatAverageDelta,
|
|
127
128
|
formatDate,
|
|
128
129
|
formatDateRange,
|
|
129
130
|
formatDeltaCopy,
|
|
130
131
|
formatIsoDate,
|
|
131
132
|
formatNumber,
|
|
132
133
|
formatRatio,
|
|
134
|
+
formatWindowCountDelta,
|
|
133
135
|
ga4AiReferralHistoryEntrySchema,
|
|
134
136
|
ga4SessionHistoryEntrySchema,
|
|
135
137
|
ga4SocialReferralHistoryEntrySchema,
|
|
@@ -178,6 +180,7 @@ import {
|
|
|
178
180
|
notImplemented,
|
|
179
181
|
notificationDtoSchema,
|
|
180
182
|
parseInclusiveEndMs,
|
|
183
|
+
parseReportPeriodDays,
|
|
181
184
|
parseRunError,
|
|
182
185
|
parseWindow,
|
|
183
186
|
pickClusterRepresentative,
|
|
@@ -196,6 +199,7 @@ import {
|
|
|
196
199
|
registrableDomain,
|
|
197
200
|
reportActionCategoryLabel,
|
|
198
201
|
reportActionTone,
|
|
202
|
+
reportComparisonWindowDays,
|
|
199
203
|
reportConfidenceLabel,
|
|
200
204
|
reportHorizonLabel,
|
|
201
205
|
reportSeverityLabel,
|
|
@@ -259,7 +263,7 @@ import {
|
|
|
259
263
|
wordpressSchemaDeployResultDtoSchema,
|
|
260
264
|
wordpressSchemaStatusResultDtoSchema,
|
|
261
265
|
wordpressStatusDtoSchema
|
|
262
|
-
} from "./chunk-
|
|
266
|
+
} from "./chunk-E2FDXNMW.js";
|
|
263
267
|
|
|
264
268
|
// src/intelligence-service.ts
|
|
265
269
|
import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
@@ -1299,6 +1303,7 @@ var adsConnections = sqliteTable("ads_connections", {
|
|
|
1299
1303
|
timezone: text("timezone"),
|
|
1300
1304
|
status: text("status"),
|
|
1301
1305
|
lastSyncedAt: text("last_synced_at"),
|
|
1306
|
+
conversionTrackingConfigured: integer("conversion_tracking_configured", { mode: "boolean" }).notNull().default(false),
|
|
1302
1307
|
createdAt: text("created_at").notNull(),
|
|
1303
1308
|
updatedAt: text("updated_at").notNull()
|
|
1304
1309
|
}, (table) => [
|
|
@@ -1362,6 +1367,7 @@ var adsInsightsDaily = sqliteTable("ads_insights_daily", {
|
|
|
1362
1367
|
impressions: integer("impressions").notNull().default(0),
|
|
1363
1368
|
clicks: integer("clicks").notNull().default(0),
|
|
1364
1369
|
spendMicros: integer("spend_micros").notNull().default(0),
|
|
1370
|
+
conversions: integer("conversions").notNull().default(0),
|
|
1365
1371
|
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
|
|
1366
1372
|
}, (table) => [
|
|
1367
1373
|
uniqueIndex("uniq_ads_insights_daily").on(table.projectId, table.level, table.entityId, table.date),
|
|
@@ -3197,6 +3203,14 @@ var MIGRATION_VERSIONS = [
|
|
|
3197
3203
|
)`,
|
|
3198
3204
|
`CREATE INDEX IF NOT EXISTS idx_gbp_attributes_loc ON gbp_attributes_snapshots(project_id, location_name, synced_at)`
|
|
3199
3205
|
]
|
|
3206
|
+
},
|
|
3207
|
+
{
|
|
3208
|
+
version: 83,
|
|
3209
|
+
name: "ads-insights-conversions",
|
|
3210
|
+
statements: [
|
|
3211
|
+
`ALTER TABLE ads_insights_daily ADD COLUMN conversions INTEGER NOT NULL DEFAULT 0`,
|
|
3212
|
+
`ALTER TABLE ads_connections ADD COLUMN conversion_tracking_configured INTEGER NOT NULL DEFAULT 0`
|
|
3213
|
+
]
|
|
3200
3214
|
}
|
|
3201
3215
|
];
|
|
3202
3216
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -5237,10 +5251,13 @@ function smoothedRunDelta(points, valueFn, maxWindow = SMOOTHED_RUN_DELTA_MAX_WI
|
|
|
5237
5251
|
const sum = (arr) => arr.reduce((s, p) => s + valueFn(p), 0);
|
|
5238
5252
|
const currentAvg = sum(tail) / tail.length;
|
|
5239
5253
|
const priorAvg = sum(prior) / prior.length;
|
|
5254
|
+
const current = roundTo1Decimal(currentAvg);
|
|
5255
|
+
const prior_ = roundTo1Decimal(priorAvg);
|
|
5240
5256
|
return {
|
|
5241
|
-
current
|
|
5242
|
-
prior:
|
|
5257
|
+
current,
|
|
5258
|
+
prior: prior_,
|
|
5243
5259
|
deltaAbs: currentAvg - priorAvg,
|
|
5260
|
+
deltaPct: deltaPercent(current, prior_),
|
|
5244
5261
|
window
|
|
5245
5262
|
};
|
|
5246
5263
|
}
|
|
@@ -10123,27 +10140,24 @@ function renderRateDeltaTile(label, delta, unit) {
|
|
|
10123
10140
|
return `<div class="metric"><div class="label">${escapeHtml(label)}</div><div class="value">\u2014</div><div class="delta">No prior data</div></div>`;
|
|
10124
10141
|
}
|
|
10125
10142
|
const valueSuffix = unit === "%" ? "%" : "";
|
|
10126
|
-
const
|
|
10127
|
-
const deltaText = `${deltaSign}${delta.deltaAbs.toFixed(unit === "%" ? 1 : 0)}${valueSuffix} vs ${delta.prior}${valueSuffix}`;
|
|
10143
|
+
const deltaText = unit === "%" ? `${delta.deltaAbs > 0 ? "+" : ""}${delta.deltaAbs.toFixed(1)}% vs ${delta.prior}%` : formatAverageDelta(delta);
|
|
10128
10144
|
return `<div class="metric">
|
|
10129
10145
|
<div class="label">${escapeHtml(label)}</div>
|
|
10130
10146
|
<div class="value ${deltaToneClass(delta.direction)}">${delta.current}${valueSuffix} <span style="font-size:14px;font-weight:500;">${deltaArrow(delta.direction)}</span></div>
|
|
10131
10147
|
<div class="delta">${deltaText}</div>
|
|
10132
10148
|
</div>`;
|
|
10133
10149
|
}
|
|
10134
|
-
function renderTrafficDeltaTile(label, delta, countLabel) {
|
|
10150
|
+
function renderTrafficDeltaTile(label, delta, countLabel, comparisonWindowDays) {
|
|
10135
10151
|
if (!delta) {
|
|
10136
10152
|
return `<div class="metric"><div class="label">${escapeHtml(label)}</div><div class="value">\u2014</div><div class="delta">Not enough trend data</div></div>`;
|
|
10137
10153
|
}
|
|
10138
|
-
const
|
|
10139
|
-
const deltaText = `${deltaSign}${formatNumber(delta.deltaAbs)} ${countLabel} vs prior ${WHATS_CHANGED_PERIOD_DAYS} days`;
|
|
10154
|
+
const deltaText = formatWindowCountDelta(delta, countLabel, `vs prior ${comparisonWindowDays} days`);
|
|
10140
10155
|
return `<div class="metric">
|
|
10141
10156
|
<div class="label">${escapeHtml(label)}</div>
|
|
10142
10157
|
<div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${deltaArrow(delta.direction)}</span></div>
|
|
10143
10158
|
<div class="delta">${deltaText}</div>
|
|
10144
10159
|
</div>`;
|
|
10145
10160
|
}
|
|
10146
|
-
var WHATS_CHANGED_PERIOD_DAYS = 14;
|
|
10147
10161
|
function renderProviderMovements(movements, audience) {
|
|
10148
10162
|
const meaningful = movements.filter((m) => m.direction !== "flat");
|
|
10149
10163
|
if (meaningful.length === 0) return "";
|
|
@@ -10210,8 +10224,8 @@ function renderWhatsChanged(report, audience) {
|
|
|
10210
10224
|
${renderRateDeltaTile(isClient ? "AI links to your website" : "Citation rate", w.citationRate, "%")}
|
|
10211
10225
|
${renderRateDeltaTile(isClient ? "AI mentions your name" : "Mention rate", w.mentionRate, "%")}
|
|
10212
10226
|
${renderRateDeltaTile(isClient ? "Questions AI answered with you" : "Cited queries", w.citedQueryCount, "count")}
|
|
10213
|
-
${renderTrafficDeltaTile(isClient ? "Visitors from Google" : "GSC clicks", w.gscClicksDelta, isClient ? "visits" : "clicks")}
|
|
10214
|
-
${renderTrafficDeltaTile(isClient ? "Visitors from AI tools" : "AI referral sessions", w.aiReferralsDelta, isClient ? "visits" : "sessions")}
|
|
10227
|
+
${renderTrafficDeltaTile(isClient ? "Visitors from Google" : "GSC clicks", w.gscClicksDelta, isClient ? "visits" : "clicks", w.comparisonWindowDays)}
|
|
10228
|
+
${renderTrafficDeltaTile(isClient ? "Visitors from AI tools" : "AI referral sessions", w.aiReferralsDelta, isClient ? "visits" : "sessions", w.comparisonWindowDays)}
|
|
10215
10229
|
</div>`;
|
|
10216
10230
|
const movements = renderProviderMovements(w.providerMovements, audience);
|
|
10217
10231
|
const winsHeading = isClient ? "What got better" : "Wins";
|
|
@@ -10676,33 +10690,36 @@ function renderAiReferrals(report) {
|
|
|
10676
10690
|
</div>`
|
|
10677
10691
|
);
|
|
10678
10692
|
}
|
|
10679
|
-
function serverActivityHeading(audience, hasData) {
|
|
10693
|
+
function serverActivityHeading(audience, hasData, windowDays) {
|
|
10680
10694
|
const isClient = audience === "client";
|
|
10681
10695
|
return {
|
|
10682
10696
|
id: "server-activity",
|
|
10683
10697
|
eyebrow: isClient ? "AI engine attention" : "Section 10",
|
|
10684
10698
|
title: "AI Visibility \u2014 Server-Side",
|
|
10685
|
-
intro: isClient ? hasData ?
|
|
10699
|
+
intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${windowDays} days \u2014 the other half of citations.` : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
|
|
10686
10700
|
};
|
|
10687
10701
|
}
|
|
10688
10702
|
function renderServerActivity(report, audience) {
|
|
10689
10703
|
const sa = report.serverActivity;
|
|
10690
10704
|
const isClient = audience === "client";
|
|
10705
|
+
const windowDays = report.meta.periodDays;
|
|
10706
|
+
const windowLabel = `${windowDays}d`;
|
|
10707
|
+
const priorWindowLabel = `vs prior ${windowDays} days`;
|
|
10691
10708
|
if (!sa) {
|
|
10692
10709
|
if (isClient) return "";
|
|
10693
10710
|
return section(
|
|
10694
|
-
serverActivityHeading("agency", false),
|
|
10711
|
+
serverActivityHeading("agency", false, windowDays),
|
|
10695
10712
|
renderEmpty("Connect a server-side traffic source to surface what AI engines do directly in your server logs \u2014 distinct from GA4 click-throughs.")
|
|
10696
10713
|
);
|
|
10697
10714
|
}
|
|
10698
10715
|
if (!sa.hasData) {
|
|
10699
10716
|
return section(
|
|
10700
|
-
serverActivityHeading(audience, false),
|
|
10717
|
+
serverActivityHeading(audience, false, windowDays),
|
|
10701
10718
|
renderEmpty(isClient ? "Your server-side traffic source is connected. Numbers will appear after the next sync." : "Source connected \u2014 collecting your first data. Numbers will appear after the next sync.")
|
|
10702
10719
|
);
|
|
10703
10720
|
}
|
|
10704
10721
|
const formatDelta = (d, suffix) => {
|
|
10705
|
-
const copy = formatDeltaCopy(d, suffix);
|
|
10722
|
+
const copy = formatDeltaCopy(d, suffix, priorWindowLabel);
|
|
10706
10723
|
if (!copy) return "";
|
|
10707
10724
|
return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml(copy)}</span>`;
|
|
10708
10725
|
};
|
|
@@ -10729,7 +10746,7 @@ function renderServerActivity(report, audience) {
|
|
|
10729
10746
|
<td class="numeric">${formatNumber(o.referralArrivals)}</td>
|
|
10730
10747
|
</tr>`).join("");
|
|
10731
10748
|
return section(
|
|
10732
|
-
serverActivityHeading("client", true),
|
|
10749
|
+
serverActivityHeading("client", true, windowDays),
|
|
10733
10750
|
`<div class="metric-grid">
|
|
10734
10751
|
<div class="metric">
|
|
10735
10752
|
<div class="label">AI bot requests observed</div>
|
|
@@ -10749,7 +10766,7 @@ function renderServerActivity(report, audience) {
|
|
|
10749
10766
|
</div>
|
|
10750
10767
|
${clientOperatorRows ? `<div class="chart-card"><h3>By AI tool</h3>
|
|
10751
10768
|
<table class="report-table">
|
|
10752
|
-
<thead><tr><th>AI tool</th><th class="numeric">Bot requests (
|
|
10769
|
+
<thead><tr><th>AI tool</th><th class="numeric">Bot requests (${windowLabel})</th><th class="numeric">User fetches (${windowLabel})</th><th class="numeric">Referral sessions</th></tr></thead>
|
|
10753
10770
|
<tbody>${clientOperatorRows}</tbody>
|
|
10754
10771
|
</table>
|
|
10755
10772
|
<p class="meta">Bot requests are bulk crawl (GPTBot, PerplexityBot, \u2026). User fetches are on-demand reads triggered by real users inside an AI surface (ChatGPT-User, Perplexity-User, \u2026). Verified means the request came from an IP the operator publishes as its own; unverified means the user-agent matched but the IP is not in a published range. User-fetch totals count both, since many genuine user fetches come from outside any published range.</p>
|
|
@@ -10790,28 +10807,28 @@ function renderServerActivity(report, audience) {
|
|
|
10790
10807
|
const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
|
|
10791
10808
|
sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
|
|
10792
10809
|
COLORS.series[1],
|
|
10793
|
-
|
|
10810
|
+
`Verified crawler hits over time (last ${windowDays} days)`
|
|
10794
10811
|
) : "";
|
|
10795
10812
|
return section(
|
|
10796
|
-
serverActivityHeading("agency", true),
|
|
10813
|
+
serverActivityHeading("agency", true, windowDays),
|
|
10797
10814
|
`<div class="metric-grid">
|
|
10798
10815
|
<div class="metric">
|
|
10799
|
-
<div class="label">Verified crawler hits (
|
|
10816
|
+
<div class="label">Verified crawler hits (${windowLabel})</div>
|
|
10800
10817
|
<div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
|
|
10801
10818
|
<div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "hits")}</div>
|
|
10802
10819
|
</div>
|
|
10803
10820
|
<div class="metric">
|
|
10804
|
-
<div class="label">Unverified crawler hits (
|
|
10821
|
+
<div class="label">Unverified crawler hits (${windowLabel})</div>
|
|
10805
10822
|
<div class="value">${formatNumber(sa.unverifiedCrawlerHits.current)}</div>
|
|
10806
10823
|
<div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits, "hits")}</div>
|
|
10807
10824
|
</div>
|
|
10808
10825
|
<div class="metric">
|
|
10809
|
-
<div class="label">AI user-fetch hits (
|
|
10826
|
+
<div class="label">AI user-fetch hits (${windowLabel})</div>
|
|
10810
10827
|
<div class="value">${formatNumber(sa.aiUserFetchHits.current)}</div>
|
|
10811
10828
|
<div class="subtitle">${formatDelta(sa.aiUserFetchHits, "hits")}</div>
|
|
10812
10829
|
</div>
|
|
10813
10830
|
<div class="metric">
|
|
10814
|
-
<div class="label">AI-referral sessions (
|
|
10831
|
+
<div class="label">AI-referral sessions (${windowLabel})</div>
|
|
10815
10832
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
10816
10833
|
<div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
|
|
10817
10834
|
</div>
|
|
@@ -10820,12 +10837,12 @@ function renderServerActivity(report, audience) {
|
|
|
10820
10837
|
${operatorRows ? `<div class="chart-card"><h3>Per AI operator</h3>
|
|
10821
10838
|
<p class="meta">Verified means the request's source IP falls inside the operator's published range. Unverified bots claim the user-agent but the IP is not in a published range, so it could be the real bot or an imitator. User fetches are on-demand reads from an AI surface on behalf of a real user (ChatGPT-User, Perplexity-User, \u2026), disjoint from bulk crawl and counted whether or not the IP can be verified.</p>
|
|
10822
10839
|
<table class="report-table">
|
|
10823
|
-
<thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">User fetches</th><th class="numeric">Referral sessions</th><th class="numeric"
|
|
10840
|
+
<thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">User fetches</th><th class="numeric">Referral sessions</th><th class="numeric">${windowLabel} delta</th></tr></thead>
|
|
10824
10841
|
<tbody>${operatorRows}</tbody>
|
|
10825
10842
|
</table>
|
|
10826
10843
|
</div>` : ""}
|
|
10827
10844
|
${pathRows ? `<div class="chart-card"><h3>Top crawled paths</h3>
|
|
10828
|
-
<p class="meta">Pages AI bots fetched most often (verified only, last
|
|
10845
|
+
<p class="meta">Pages AI bots fetched most often (verified only, last ${windowLabel}).</p>
|
|
10829
10846
|
<table class="report-table">
|
|
10830
10847
|
<thead><tr><th>Path</th><th class="numeric">Verified hits</th><th class="numeric">Distinct operators</th></tr></thead>
|
|
10831
10848
|
<tbody>${pathRows}</tbody>
|
|
@@ -11305,7 +11322,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
11305
11322
|
<header class="header">
|
|
11306
11323
|
<div class="eyebrow">AI Visibility Report</div>
|
|
11307
11324
|
<h1>${escapeHtml(report.meta.project.displayName)}</h1>
|
|
11308
|
-
<div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
|
|
11325
|
+
<div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Last ${report.meta.periodDays} days \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
|
|
11309
11326
|
</header>
|
|
11310
11327
|
${sections}
|
|
11311
11328
|
<footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml(formatIsoDate(report.meta.generatedAt))}</footer>
|
|
@@ -11321,10 +11338,8 @@ var TOP_LANDING_PAGES_LIMIT = 20;
|
|
|
11321
11338
|
var TOP_AI_REFERRAL_PAGES_LIMIT = 10;
|
|
11322
11339
|
var TOP_CAMPAIGN_LIMIT = 10;
|
|
11323
11340
|
var INSIGHT_LOOKBACK_RUNS = 5;
|
|
11324
|
-
var REPORT_WINDOW_DAYS = 30;
|
|
11325
|
-
var SERVER_ACTIVITY_HEADLINE_DAYS = 7;
|
|
11326
|
-
var SERVER_ACTIVITY_TREND_DAYS = 14;
|
|
11327
11341
|
var SERVER_ACTIVITY_TOP_PATHS_LIMIT = 10;
|
|
11342
|
+
var GA_WINDOW_SUMMARY_KEYS = { 7: "7d", 30: "30d", 90: "90d" };
|
|
11328
11343
|
function windowStartDate(endDate, windowDays) {
|
|
11329
11344
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(endDate);
|
|
11330
11345
|
if (!m) return endDate;
|
|
@@ -11370,12 +11385,12 @@ function loadQueryLookup(db, projectId) {
|
|
|
11370
11385
|
for (const row of rows) byId.set(row.id, row.query);
|
|
11371
11386
|
return { byId };
|
|
11372
11387
|
}
|
|
11373
|
-
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries) {
|
|
11388
|
+
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
|
|
11374
11389
|
const allRows = db.select().from(gscSearchData).where(eq14(gscSearchData.projectId, projectId)).all();
|
|
11375
11390
|
if (allRows.length === 0) return null;
|
|
11376
11391
|
let maxDate = "";
|
|
11377
11392
|
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
11378
|
-
const startDate = windowStartDate(maxDate,
|
|
11393
|
+
const startDate = windowStartDate(maxDate, windowDays);
|
|
11379
11394
|
const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
11380
11395
|
if (rows.length === 0) return null;
|
|
11381
11396
|
let totalClicks = 0;
|
|
@@ -11442,19 +11457,20 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
11442
11457
|
gscButNotTracked
|
|
11443
11458
|
};
|
|
11444
11459
|
}
|
|
11445
|
-
function buildGaSection(db, projectId) {
|
|
11446
|
-
const
|
|
11460
|
+
function buildGaSection(db, projectId, windowDays) {
|
|
11461
|
+
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
|
|
11462
|
+
const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
|
|
11447
11463
|
and9(
|
|
11448
11464
|
eq14(gaTrafficWindowSummaries.projectId, projectId),
|
|
11449
|
-
eq14(gaTrafficWindowSummaries.windowKey,
|
|
11465
|
+
eq14(gaTrafficWindowSummaries.windowKey, gaWindowKey)
|
|
11450
11466
|
)
|
|
11451
|
-
).limit(1).get();
|
|
11467
|
+
).limit(1).get() : void 0;
|
|
11452
11468
|
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq14(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
11453
11469
|
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
|
|
11454
11470
|
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
11455
11471
|
let snapshotMaxDate = "";
|
|
11456
11472
|
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
11457
|
-
const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate,
|
|
11473
|
+
const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, windowDays) : "";
|
|
11458
11474
|
const snapshotRows = snapshotStartDate ? allSnapshotRows.filter((r) => r.date >= snapshotStartDate && r.date <= snapshotMaxDate) : allSnapshotRows;
|
|
11459
11475
|
const totalSessions = windowSummary?.totalSessions ?? fallbackSummary?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
|
|
11460
11476
|
const totalUsers = windowSummary?.totalUsers ?? fallbackSummary?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
|
|
@@ -11620,7 +11636,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
11620
11636
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.woff2'
|
|
11621
11637
|
`;
|
|
11622
11638
|
}
|
|
11623
|
-
function buildServerActivity(db, projectId) {
|
|
11639
|
+
function buildServerActivity(db, projectId, windowDays) {
|
|
11624
11640
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
11625
11641
|
and9(
|
|
11626
11642
|
eq14(trafficSources.projectId, projectId),
|
|
@@ -11630,9 +11646,9 @@ function buildServerActivity(db, projectId) {
|
|
|
11630
11646
|
if (sourceRows.length === 0) return null;
|
|
11631
11647
|
const now = /* @__PURE__ */ new Date();
|
|
11632
11648
|
const headlineEnd = now.toISOString();
|
|
11633
|
-
const headlineStartMs = now.getTime() -
|
|
11634
|
-
const priorStartMs = headlineStartMs -
|
|
11635
|
-
const trendStartMs =
|
|
11649
|
+
const headlineStartMs = now.getTime() - windowDays * 24 * 60 * 6e4;
|
|
11650
|
+
const priorStartMs = headlineStartMs - windowDays * 24 * 60 * 6e4;
|
|
11651
|
+
const trendStartMs = headlineStartMs;
|
|
11636
11652
|
const headlineStart = new Date(headlineStartMs).toISOString();
|
|
11637
11653
|
const priorStart = new Date(priorStartMs).toISOString();
|
|
11638
11654
|
const trendStart = new Date(trendStartMs).toISOString();
|
|
@@ -12416,8 +12432,6 @@ function buildAgencyDiagnostics(input) {
|
|
|
12416
12432
|
diagnostics
|
|
12417
12433
|
};
|
|
12418
12434
|
}
|
|
12419
|
-
var WHATS_CHANGED_PERIOD_DAYS2 = 14;
|
|
12420
|
-
var WHATS_CHANGED_MIN_TREND_POINTS = WHATS_CHANGED_PERIOD_DAYS2 * 2;
|
|
12421
12435
|
var RATE_REAL_MOVEMENT_THRESHOLD_PP = 3;
|
|
12422
12436
|
var COUNT_REAL_MOVEMENT_THRESHOLD = 0.5;
|
|
12423
12437
|
var WIN_REGRESSION_LIMIT = 5;
|
|
@@ -12426,10 +12440,10 @@ function rateDirection(delta, threshold = 0.5) {
|
|
|
12426
12440
|
if (delta < -threshold) return "down";
|
|
12427
12441
|
return "flat";
|
|
12428
12442
|
}
|
|
12429
|
-
function periodOverPeriodDelta(trend) {
|
|
12430
|
-
if (trend.length <
|
|
12431
|
-
const tail = trend.slice(-
|
|
12432
|
-
const prior = trend.slice(-
|
|
12443
|
+
function periodOverPeriodDelta(trend, halfWindow) {
|
|
12444
|
+
if (trend.length < halfWindow * 2) return null;
|
|
12445
|
+
const tail = trend.slice(-halfWindow);
|
|
12446
|
+
const prior = trend.slice(-halfWindow * 2, -halfWindow);
|
|
12433
12447
|
const current = tail.reduce((s, p) => s + p.value, 0);
|
|
12434
12448
|
const priorTotal = prior.reduce((s, p) => s + p.value, 0);
|
|
12435
12449
|
const deltaAbs = current - priorTotal;
|
|
@@ -12437,10 +12451,11 @@ function periodOverPeriodDelta(trend) {
|
|
|
12437
12451
|
current,
|
|
12438
12452
|
prior: priorTotal,
|
|
12439
12453
|
deltaAbs,
|
|
12454
|
+
deltaPct: deltaPercent(current, priorTotal),
|
|
12440
12455
|
direction: rateDirection(deltaAbs, 0)
|
|
12441
12456
|
};
|
|
12442
12457
|
}
|
|
12443
|
-
function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHistory, trendLength) {
|
|
12458
|
+
function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHistory, trendLength, comparisonWindowDays) {
|
|
12444
12459
|
if (!enoughHistory) {
|
|
12445
12460
|
return `Building baseline (${trendLength} of ${MIN_TREND_POINTS} checks completed). Trends appear after a few more checks.`;
|
|
12446
12461
|
}
|
|
@@ -12453,15 +12468,15 @@ function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHisto
|
|
|
12453
12468
|
}
|
|
12454
12469
|
if (aiReferrals && aiReferrals.direction !== "flat") {
|
|
12455
12470
|
const arrow = aiReferrals.direction === "up" ? "\u2191" : "\u2193";
|
|
12456
|
-
parts.push(`AI referrals ${arrow}${Math.abs(aiReferrals.deltaAbs)} sessions vs prior
|
|
12471
|
+
parts.push(`AI referrals ${arrow}${Math.abs(aiReferrals.deltaAbs)} sessions vs prior ${comparisonWindowDays} days`);
|
|
12457
12472
|
} else if (gscClicks && gscClicks.direction !== "flat") {
|
|
12458
12473
|
const arrow = gscClicks.direction === "up" ? "\u2191" : "\u2193";
|
|
12459
|
-
parts.push(`GSC clicks ${arrow}${Math.abs(gscClicks.deltaAbs)} vs prior
|
|
12474
|
+
parts.push(`GSC clicks ${arrow}${Math.abs(gscClicks.deltaAbs)} vs prior ${comparisonWindowDays} days`);
|
|
12460
12475
|
}
|
|
12461
12476
|
return parts.length > 0 ? `${parts.join(" \xB7 ")}.` : "No meaningful movement vs the prior period.";
|
|
12462
12477
|
}
|
|
12463
12478
|
function buildWhatsChanged(input) {
|
|
12464
|
-
const { citationsTrend, gsc, aiReferrals, insights: insightList } = input;
|
|
12479
|
+
const { citationsTrend, gsc, aiReferrals, insights: insightList, comparisonWindowDays } = input;
|
|
12465
12480
|
const baseline = isTrendBaseline(citationsTrend);
|
|
12466
12481
|
const latest = citationsTrend.at(-1);
|
|
12467
12482
|
const prior = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
|
|
@@ -12498,8 +12513,8 @@ function buildWhatsChanged(input) {
|
|
|
12498
12513
|
}
|
|
12499
12514
|
providerMovements.sort((a, b) => Math.abs(b.deltaAbs) - Math.abs(a.deltaAbs));
|
|
12500
12515
|
}
|
|
12501
|
-
const gscClicksDelta = gsc ? periodOverPeriodDelta(gsc.trend.map((t) => ({ date: t.date, value: t.clicks }))) : null;
|
|
12502
|
-
const aiReferralsDelta = aiReferrals ? periodOverPeriodDelta(aiReferrals.trend.map((t) => ({ date: t.date, value: t.sessions }))) : null;
|
|
12516
|
+
const gscClicksDelta = gsc ? periodOverPeriodDelta(gsc.trend.map((t) => ({ date: t.date, value: t.clicks })), comparisonWindowDays) : null;
|
|
12517
|
+
const aiReferralsDelta = aiReferrals ? periodOverPeriodDelta(aiReferrals.trend.map((t) => ({ date: t.date, value: t.sessions })), comparisonWindowDays) : null;
|
|
12503
12518
|
const wins = insightList.filter((i) => i.type === "gain").slice(0, WIN_REGRESSION_LIMIT);
|
|
12504
12519
|
const regressions = insightList.filter((i) => i.type === "regression").slice(0, WIN_REGRESSION_LIMIT);
|
|
12505
12520
|
const headline = buildWhatsChangedHeadline(
|
|
@@ -12507,7 +12522,8 @@ function buildWhatsChanged(input) {
|
|
|
12507
12522
|
gscClicksDelta,
|
|
12508
12523
|
aiReferralsDelta,
|
|
12509
12524
|
enoughHistory,
|
|
12510
|
-
citationsTrend.length
|
|
12525
|
+
citationsTrend.length,
|
|
12526
|
+
comparisonWindowDays
|
|
12511
12527
|
);
|
|
12512
12528
|
return {
|
|
12513
12529
|
enoughHistory,
|
|
@@ -12517,14 +12533,16 @@ function buildWhatsChanged(input) {
|
|
|
12517
12533
|
citedQueryCount,
|
|
12518
12534
|
gscClicksDelta,
|
|
12519
12535
|
aiReferralsDelta,
|
|
12536
|
+
comparisonWindowDays,
|
|
12520
12537
|
providerMovements,
|
|
12521
12538
|
wins,
|
|
12522
12539
|
regressions
|
|
12523
12540
|
};
|
|
12524
12541
|
}
|
|
12525
|
-
function buildProjectReport(db, projectName) {
|
|
12542
|
+
function buildProjectReport(db, projectName, periodDays) {
|
|
12526
12543
|
const project = resolveProject(db, projectName);
|
|
12527
12544
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
12545
|
+
const comparisonWindowDays = reportComparisonWindowDays(periodDays);
|
|
12528
12546
|
const allRuns = db.select().from(runs).where(and9(eq14(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
|
|
12529
12547
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
12530
12548
|
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
@@ -12564,12 +12582,13 @@ function buildProjectReport(db, projectName) {
|
|
|
12564
12582
|
project.id,
|
|
12565
12583
|
projectBrandNames,
|
|
12566
12584
|
project.canonicalDomain,
|
|
12567
|
-
trackedQueries
|
|
12585
|
+
trackedQueries,
|
|
12586
|
+
periodDays
|
|
12568
12587
|
);
|
|
12569
|
-
const gaSection = buildGaSection(db, project.id);
|
|
12588
|
+
const gaSection = buildGaSection(db, project.id, periodDays);
|
|
12570
12589
|
const socialSection = buildSocialReferrals(db, project.id);
|
|
12571
12590
|
const aiReferralsSection = buildAiReferrals(db, project.id);
|
|
12572
|
-
const serverActivitySection = buildServerActivity(db, project.id);
|
|
12591
|
+
const serverActivitySection = buildServerActivity(db, project.id, periodDays);
|
|
12573
12592
|
const indexingHealthSection = buildIndexingHealth(db, project.id);
|
|
12574
12593
|
const citationsTrend = buildCitationsTrend(db, project.id, queryLookup, latestRunLocation);
|
|
12575
12594
|
const insightList = buildInsightList(db, project.id, latestRunLocation);
|
|
@@ -12588,7 +12607,8 @@ function buildProjectReport(db, projectName) {
|
|
|
12588
12607
|
citationsTrend,
|
|
12589
12608
|
gsc: gscSection,
|
|
12590
12609
|
aiReferrals: aiReferralsSection,
|
|
12591
|
-
insights: insightList
|
|
12610
|
+
insights: insightList,
|
|
12611
|
+
comparisonWindowDays
|
|
12592
12612
|
});
|
|
12593
12613
|
const totalQueryCount = queryLookup.byId.size;
|
|
12594
12614
|
const citedQueryIds = /* @__PURE__ */ new Set();
|
|
@@ -12704,7 +12724,8 @@ function buildProjectReport(db, projectName) {
|
|
|
12704
12724
|
location: reportLocation,
|
|
12705
12725
|
providerLocationHandling,
|
|
12706
12726
|
periodStart,
|
|
12707
|
-
periodEnd
|
|
12727
|
+
periodEnd,
|
|
12728
|
+
periodDays
|
|
12708
12729
|
},
|
|
12709
12730
|
executiveSummary,
|
|
12710
12731
|
citationScorecard,
|
|
@@ -12740,12 +12761,14 @@ function reportFilenameFor(project, generatedAt, audience) {
|
|
|
12740
12761
|
}
|
|
12741
12762
|
async function reportRoutes(app) {
|
|
12742
12763
|
app.get("/projects/:name/report", async (request, reply) => {
|
|
12743
|
-
const
|
|
12764
|
+
const periodDays = parseReportPeriodDays(request.query.period);
|
|
12765
|
+
const dto = buildProjectReport(app.db, request.params.name, periodDays);
|
|
12744
12766
|
return reply.send(dto);
|
|
12745
12767
|
});
|
|
12746
12768
|
app.get("/projects/:name/report.html", async (request, reply) => {
|
|
12747
12769
|
const audience = parseReportAudience(request.query.audience);
|
|
12748
|
-
const
|
|
12770
|
+
const periodDays = parseReportPeriodDays(request.query.period);
|
|
12771
|
+
const dto = buildProjectReport(app.db, request.params.name, periodDays);
|
|
12749
12772
|
const html = renderReportHtml(dto, { audience });
|
|
12750
12773
|
const filename = reportFilenameFor(dto.meta.project, dto.meta.generatedAt, audience);
|
|
12751
12774
|
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
@@ -14105,6 +14128,12 @@ var analyticsWindowParameter = {
|
|
|
14105
14128
|
description: "Time window for analytics queries.",
|
|
14106
14129
|
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
14107
14130
|
};
|
|
14131
|
+
var reportPeriodQueryParameter = {
|
|
14132
|
+
name: "period",
|
|
14133
|
+
in: "query",
|
|
14134
|
+
description: "Report window in days \u2014 one of 7, 14, 30, 90 (default 30). Scopes the GSC, GA4, and server-side AI activity sections and the period-over-period comparisons to this window.",
|
|
14135
|
+
schema: { type: "integer", enum: [7, 14, 30, 90] }
|
|
14136
|
+
};
|
|
14108
14137
|
var sinceQueryParameter = {
|
|
14109
14138
|
name: "since",
|
|
14110
14139
|
in: "query",
|
|
@@ -16839,7 +16868,7 @@ var routeCatalog = [
|
|
|
16839
16868
|
summary: "Aggregated canonical AEO report",
|
|
16840
16869
|
tags: ["report"],
|
|
16841
16870
|
description: "Bundles every section the canonry-report HTML output needs (executive summary, client summary, agency diagnostics, action plan, 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 canonical JSON payload. Backs `canonry report <project>` and MCP report reads.",
|
|
16842
|
-
parameters: [nameParameter],
|
|
16871
|
+
parameters: [nameParameter, reportPeriodQueryParameter],
|
|
16843
16872
|
responses: {
|
|
16844
16873
|
200: jsonResponse("Report returned.", "ProjectReportDto"),
|
|
16845
16874
|
404: errorResponse("Project not found.")
|
|
@@ -16851,7 +16880,7 @@ var routeCatalog = [
|
|
|
16851
16880
|
summary: "Standalone HTML AEO report",
|
|
16852
16881
|
tags: ["report"],
|
|
16853
16882
|
description: "Server-rendered self-contained HTML version of the project report. Same data as `/projects/{name}/report` (JSON), rendered through the canonry HTML report renderer in agency or client mode. Returns `text/html` with `Content-Disposition: attachment` so browsers download it as `canonry-report-<project>-<audience>-YYYY-MM-DD.html`. Open in a browser and Print \u2192 Save as PDF for a PDF copy.",
|
|
16854
|
-
parameters: [nameParameter, reportAudienceQueryParameter],
|
|
16883
|
+
parameters: [nameParameter, reportAudienceQueryParameter, reportPeriodQueryParameter],
|
|
16855
16884
|
responses: {
|
|
16856
16885
|
200: { description: "HTML report returned.", content: { "text/html": { schema: { type: "string" } } } },
|
|
16857
16886
|
404: errorResponse("Project not found.")
|
|
@@ -21508,7 +21537,8 @@ function statusDto(row) {
|
|
|
21508
21537
|
currencyCode: row.currencyCode,
|
|
21509
21538
|
timezone: row.timezone,
|
|
21510
21539
|
status: row.status,
|
|
21511
|
-
lastSyncedAt: row.lastSyncedAt
|
|
21540
|
+
lastSyncedAt: row.lastSyncedAt,
|
|
21541
|
+
conversionTrackingConfigured: row.conversionTrackingConfigured
|
|
21512
21542
|
};
|
|
21513
21543
|
}
|
|
21514
21544
|
function creativeDto(raw) {
|
|
@@ -21706,6 +21736,7 @@ async function adsRoutes(app, opts) {
|
|
|
21706
21736
|
impressions: row.impressions,
|
|
21707
21737
|
clicks: row.clicks,
|
|
21708
21738
|
spendMicros: row.spendMicros,
|
|
21739
|
+
conversions: row.conversions,
|
|
21709
21740
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
21710
21741
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
21711
21742
|
}));
|
|
@@ -21726,12 +21757,14 @@ async function adsRoutes(app, opts) {
|
|
|
21726
21757
|
let impressions = 0;
|
|
21727
21758
|
let clicks = 0;
|
|
21728
21759
|
let spendMicros = 0;
|
|
21760
|
+
let conversions = 0;
|
|
21729
21761
|
let fromDate = null;
|
|
21730
21762
|
let toDate = null;
|
|
21731
21763
|
for (const insight of campaignInsights) {
|
|
21732
21764
|
impressions += insight.impressions;
|
|
21733
21765
|
clicks += insight.clicks;
|
|
21734
21766
|
spendMicros += insight.spendMicros;
|
|
21767
|
+
conversions += insight.conversions;
|
|
21735
21768
|
if (fromDate === null || insight.date < fromDate) fromDate = insight.date;
|
|
21736
21769
|
if (toDate === null || insight.date > toDate) toDate = insight.date;
|
|
21737
21770
|
}
|
|
@@ -21748,6 +21781,7 @@ async function adsRoutes(app, opts) {
|
|
|
21748
21781
|
impressions,
|
|
21749
21782
|
clicks,
|
|
21750
21783
|
spendMicros,
|
|
21784
|
+
conversions,
|
|
21751
21785
|
ctr: adsCtr(clicks, impressions),
|
|
21752
21786
|
cpcMicros: adsCpcMicros(spendMicros, clicks)
|
|
21753
21787
|
}
|