@canonry/canonry 5.28.1 → 5.29.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/assets/{AuditHistoryPanel-CWd9UN6L.js → AuditHistoryPanel-ru-RI1SR.js} +1 -1
- package/assets/assets/{BacklinksPage-B2gPTzyE.js → BacklinksPage-BbVWtfAh.js} +1 -1
- package/assets/assets/{HistoryPage-xtkG8SZN.js → HistoryPage-CxIxsxIv.js} +1 -1
- package/assets/assets/MeasurementPropertyPage-Oju3oAP3.js +1 -0
- package/assets/assets/ProjectPage-ZsQo28yM.js +8 -0
- package/assets/assets/{RunRow-DnSo7Nle.js → RunRow-D73cbxey.js} +1 -1
- package/assets/assets/{RunsPage-FAEuo4Ra.js → RunsPage-D9hOl6k6.js} +1 -1
- package/assets/assets/{SettingsPage-C4OZ1HB9.js → SettingsPage-By5cJJaC.js} +1 -1
- package/assets/assets/SiteHealthSection-Gfyispq_.js +4 -0
- package/assets/assets/{TrafficPage-D03S6xF6.js → TrafficPage-8khxNXDL.js} +2 -2
- package/assets/assets/{TrafficSourceDetailPage-DBu6oFFK.js → TrafficSourceDetailPage-FDe4jKLH.js} +1 -1
- package/assets/assets/{extract-error-message-BuH2CCaJ.js → extract-error-message-Co1hxrsc.js} +1 -1
- package/assets/assets/index-DKScDg92.js +86 -0
- package/assets/assets/{react-sigma_core.esm.min-CxwZsgxR.js → react-sigma_core.esm.min-DxAxqyeI.js} +1 -1
- package/assets/assets/{v2-overview-adapter-DTCKxzZT.js → v2-overview-adapter-DNbOwIJq.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-EP4STEPI.js → chunk-7BW7BAD5.js} +164 -96
- package/dist/{chunk-KVQHG3WB.js → chunk-DCGELKRW.js} +6234 -5911
- package/dist/{chunk-MPICT6V7.js → chunk-J6BJIOIN.js} +3 -3
- package/dist/{chunk-7RKCDOWO.js → chunk-KGYHEYHX.js} +238 -174
- package/dist/{chunk-ONTEFAA7.js → chunk-THMIDWMS.js} +57 -3
- package/dist/{chunk-FNGITGDF.js → chunk-VPIOF2PB.js} +2 -2
- package/dist/{chunk-DX2JUC5H.js → chunk-WPQB4UYX.js} +3 -3
- package/dist/cli.js +134 -123
- package/dist/{demo-server-HWWIMGD6.js → demo-server-EU6B4KNK.js} +24 -32
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-6LDMVI66.js → intelligence-service-FVUNBN66.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +9 -9
- package/assets/assets/MeasurementPropertyPage-DxIMU3_T.js +0 -1
- package/assets/assets/ProjectPage-CMAFZq_s.js +0 -8
- package/assets/assets/SiteHealthSection-BzMxlz8-.js +0 -4
- package/assets/assets/index-Ba-Zs45G.js +0 -86
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
GOOGLE_MARKETING_LIVE_READ_SCOPE,
|
|
62
62
|
GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX,
|
|
63
63
|
GOOGLE_MARKETING_WRITE_SCOPE,
|
|
64
|
+
GaMoverChangeBases,
|
|
64
65
|
GoogleAdsCampaignStatuses,
|
|
65
66
|
GoogleAdsConnectionStates,
|
|
66
67
|
GoogleAdsGoalConfigurationLevels,
|
|
@@ -92,6 +93,7 @@ import {
|
|
|
92
93
|
MEASUREMENT_PORTFOLIO_TIE_NOTE,
|
|
93
94
|
MEASUREMENT_PROPERTY_CITED_DOMAINS_LIMIT,
|
|
94
95
|
MIN_DOMAIN_BRAND_KEY_LENGTH,
|
|
96
|
+
MIN_PCT_BASE,
|
|
95
97
|
MIN_TREND_POINTS,
|
|
96
98
|
MODEL_ATTRIBUTION_EVENT_LIMIT,
|
|
97
99
|
MeasurementEvidenceShapes,
|
|
@@ -110,6 +112,7 @@ import {
|
|
|
110
112
|
REPORT_VISIBILITY_COPY,
|
|
111
113
|
RESEARCH_RUN_SCOPE,
|
|
112
114
|
RUN_FILL_MAX_AGE_MS,
|
|
115
|
+
RatioUnits,
|
|
113
116
|
ReportSectionIds,
|
|
114
117
|
ResearchQueryStatuses,
|
|
115
118
|
ResearchRunStatuses,
|
|
@@ -238,6 +241,7 @@ import {
|
|
|
238
241
|
brandKeyFromText,
|
|
239
242
|
brandLabelFromDomain,
|
|
240
243
|
brandMetricsDtoSchema,
|
|
244
|
+
breakdownShares,
|
|
241
245
|
buildHarvestAnchorTerms,
|
|
242
246
|
buildMeasurementExecutionIdentity,
|
|
243
247
|
buildMeasurementRunManifestV1,
|
|
@@ -351,8 +355,8 @@ import {
|
|
|
351
355
|
formatIsoDate,
|
|
352
356
|
formatIsoDateInTimeZone,
|
|
353
357
|
formatNumber,
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
formatPercent,
|
|
359
|
+
formatSignedPercent,
|
|
356
360
|
formatWindowCountDelta,
|
|
357
361
|
ga4AiReferralDailyDtoSchema,
|
|
358
362
|
ga4AiReferralHistoryEntrySchema,
|
|
@@ -361,9 +365,12 @@ import {
|
|
|
361
365
|
ga4SocialReferralHistoryEntrySchema,
|
|
362
366
|
ga4StatusDtoSchema,
|
|
363
367
|
ga4SyncResponseDtoSchema,
|
|
368
|
+
gaAttributionTrendResponseSchema,
|
|
364
369
|
gaMeasurementAnalysisDtoSchema,
|
|
365
370
|
gaMeasurementAnalysisWindowSchema,
|
|
366
371
|
gaMeasurementHostScopeSchema,
|
|
372
|
+
gaSocialReferralTrendResponseSchema,
|
|
373
|
+
gaTrafficResponseSchema,
|
|
367
374
|
gateHarvestedSearchQueries,
|
|
368
375
|
gbpAccountListResponseSchema,
|
|
369
376
|
gbpAttributesListResponseSchema,
|
|
@@ -584,6 +591,7 @@ import {
|
|
|
584
591
|
parseRunError,
|
|
585
592
|
parseStoredMeasurementPlanAnyVersion,
|
|
586
593
|
parseWindow,
|
|
594
|
+
percentOf,
|
|
587
595
|
pickClusterRepresentative,
|
|
588
596
|
prepareBrandMatchText,
|
|
589
597
|
projectConfigSchema,
|
|
@@ -665,6 +673,7 @@ import {
|
|
|
665
673
|
reportProviderRateLabel,
|
|
666
674
|
reportQueryClassLabel,
|
|
667
675
|
reportRateDeltaCopy,
|
|
676
|
+
reportRateDeltaValue,
|
|
668
677
|
reportReferralRedirectNote,
|
|
669
678
|
reportServerActivityAgencyOperatorHeaders,
|
|
670
679
|
reportServerActivityAgencyTiles,
|
|
@@ -708,6 +717,7 @@ import {
|
|
|
708
717
|
resultsClearResponseSchema,
|
|
709
718
|
resultsExportDtoSchema,
|
|
710
719
|
retryAfterDelayMs,
|
|
720
|
+
roundRatio,
|
|
711
721
|
runCompletenessDtoSchema,
|
|
712
722
|
runDetailDtoSchema,
|
|
713
723
|
runDtoSchema,
|
|
@@ -734,6 +744,7 @@ import {
|
|
|
734
744
|
segmentCrawlerHits,
|
|
735
745
|
serializeRunError,
|
|
736
746
|
settingsDtoSchema,
|
|
747
|
+
shareOf,
|
|
737
748
|
shareOfVoiceReason,
|
|
738
749
|
shareOfVoiceSummary,
|
|
739
750
|
shiftIsoCalendarDate,
|
|
@@ -825,7 +836,7 @@ import {
|
|
|
825
836
|
wordpressSchemaDeployResultDtoSchema,
|
|
826
837
|
wordpressSchemaStatusResultDtoSchema,
|
|
827
838
|
wordpressStatusDtoSchema
|
|
828
|
-
} from "./chunk-
|
|
839
|
+
} from "./chunk-DCGELKRW.js";
|
|
829
840
|
|
|
830
841
|
// src/intelligence-service.ts
|
|
831
842
|
import { eq as eq79, desc as desc34, asc as asc14, and as and66, ne as ne13, or as or18, inArray as inArray29, gte as gte21, lte as lte18, isNull as isNull14, sql as sql29, exists } from "drizzle-orm";
|
|
@@ -9059,8 +9070,7 @@ function buildDrivers(input) {
|
|
|
9059
9070
|
drivers.push(`${formatImpressions(input.gscImpressions)} GSC impressions`);
|
|
9060
9071
|
}
|
|
9061
9072
|
if (input.recentMissRate >= 0.5 && input.competitorCount > 0) {
|
|
9062
|
-
|
|
9063
|
-
drivers.push(`missed in ${pct}% of recent runs`);
|
|
9073
|
+
drivers.push(`missed in ${formatPercent(clamp01(input.recentMissRate))} of recent runs`);
|
|
9064
9074
|
}
|
|
9065
9075
|
if (input.action === "create" && input.position === null) {
|
|
9066
9076
|
drivers.push("no existing page");
|
|
@@ -9443,8 +9453,8 @@ function buildCitationScorecard(snapshots, queryLookup) {
|
|
|
9443
9453
|
}
|
|
9444
9454
|
const providerRates = providerList.map((provider) => {
|
|
9445
9455
|
const counts2 = providerCounts.get(provider) ?? { cited: 0, mentioned: 0, total: 0 };
|
|
9446
|
-
const citationRate =
|
|
9447
|
-
const mentionRate3 =
|
|
9456
|
+
const citationRate = percentOf(counts2.cited, counts2.total) ?? 0;
|
|
9457
|
+
const mentionRate3 = percentOf(counts2.mentioned, counts2.total) ?? 0;
|
|
9448
9458
|
return {
|
|
9449
9459
|
provider,
|
|
9450
9460
|
citedCount: counts2.cited,
|
|
@@ -9549,7 +9559,7 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
|
|
|
9549
9559
|
else if (ratio >= 0.2) pressureLabel = "Moderate";
|
|
9550
9560
|
else pressureLabel = "Low";
|
|
9551
9561
|
}
|
|
9552
|
-
const sharePct =
|
|
9562
|
+
const sharePct = percentOf(data.count, totalCitedSlots) ?? 0;
|
|
9553
9563
|
const theirCitedPages = [...data.pages.entries()].map(([url, qs]) => ({ url, citedFor: [...qs].sort() })).sort((a, b) => b.citedFor.length - a.citedFor.length);
|
|
9554
9564
|
return {
|
|
9555
9565
|
domain,
|
|
@@ -9584,17 +9594,37 @@ function toBreakdown(tally, competitors2) {
|
|
|
9584
9594
|
const perCompetitor = competitors2.map((c) => ({
|
|
9585
9595
|
domain: c.domain,
|
|
9586
9596
|
mentionSnapshots: tally.competitorCounts.get(c.domain) ?? 0,
|
|
9587
|
-
shareOfCompetitiveTotal:
|
|
9597
|
+
shareOfCompetitiveTotal: percentOf(tally.competitorCounts.get(c.domain) ?? 0, competitorMentionSnapshots) ?? 0
|
|
9588
9598
|
})).filter((row) => row.mentionSnapshots > 0).sort((a, b) => b.mentionSnapshots - a.mentionSnapshots || (a.domain < b.domain ? -1 : 1));
|
|
9589
9599
|
return {
|
|
9590
9600
|
projectMentionSnapshots: tally.projectMentionSnapshots,
|
|
9591
9601
|
competitorMentionSnapshots,
|
|
9602
|
+
combinedMentionSnapshots: denom,
|
|
9592
9603
|
perCompetitor,
|
|
9604
|
+
ranking: rankMentionShare(tally, competitors2, denom),
|
|
9593
9605
|
snapshotsWithAnswerText: tally.snapshotsWithAnswerText,
|
|
9594
9606
|
snapshotsTotal: tally.snapshotsTotal,
|
|
9595
|
-
score:
|
|
9607
|
+
score: percentOf(tally.projectMentionSnapshots, denom)
|
|
9596
9608
|
};
|
|
9597
9609
|
}
|
|
9610
|
+
function rankMentionShare(tally, competitors2, denom) {
|
|
9611
|
+
if (competitors2.length === 0 || denom === 0) return [];
|
|
9612
|
+
const project = {
|
|
9613
|
+
kind: "project",
|
|
9614
|
+
domain: null,
|
|
9615
|
+
mentionSnapshots: tally.projectMentionSnapshots,
|
|
9616
|
+
share: tally.projectMentionSnapshots / denom
|
|
9617
|
+
};
|
|
9618
|
+
const rivals = competitors2.map((c) => ({ domain: c.domain, mentionSnapshots: tally.competitorCounts.get(c.domain) ?? 0 })).sort((a, b) => b.mentionSnapshots - a.mentionSnapshots || (a.domain < b.domain ? -1 : 1)).map((c) => ({
|
|
9619
|
+
kind: "competitor",
|
|
9620
|
+
domain: c.domain,
|
|
9621
|
+
mentionSnapshots: c.mentionSnapshots,
|
|
9622
|
+
share: c.mentionSnapshots / denom
|
|
9623
|
+
}));
|
|
9624
|
+
const firstNotAbove = rivals.findIndex((row) => row.mentionSnapshots <= project.mentionSnapshots);
|
|
9625
|
+
const at = firstNotAbove === -1 ? rivals.length : firstNotAbove;
|
|
9626
|
+
return [...rivals.slice(0, at), project, ...rivals.slice(at)];
|
|
9627
|
+
}
|
|
9598
9628
|
function buildMentionShare(snapshots, options) {
|
|
9599
9629
|
const competitorMatchers = new Map(
|
|
9600
9630
|
options.competitors.map((c) => [c.domain, compileBrandAliases(usableBrandAliases([...c.brandTokens]))])
|
|
@@ -9691,7 +9721,7 @@ function buildMentionShare(snapshots, options) {
|
|
|
9691
9721
|
value: "No mentions",
|
|
9692
9722
|
delta: scopeLabel(scope, "No brand mentions in this run"),
|
|
9693
9723
|
tone: "neutral",
|
|
9694
|
-
description: describe({ scope,
|
|
9724
|
+
description: describe({ scope, breakdown: headline, branded: brandedBreakdown }),
|
|
9695
9725
|
tooltip,
|
|
9696
9726
|
trend: [],
|
|
9697
9727
|
scope,
|
|
@@ -9701,10 +9731,10 @@ function buildMentionShare(snapshots, options) {
|
|
|
9701
9731
|
}
|
|
9702
9732
|
return {
|
|
9703
9733
|
label: "Mention Share",
|
|
9704
|
-
value:
|
|
9734
|
+
value: formatPercent(headline.projectMentionSnapshots / denom),
|
|
9705
9735
|
delta: scopeLabel(scope, `${headline.projectMentionSnapshots} of ${denom} brand mentions`),
|
|
9706
9736
|
tone: mentionShareTone(score),
|
|
9707
|
-
description: describe({ scope,
|
|
9737
|
+
description: describe({ scope, breakdown: headline, branded: brandedBreakdown }),
|
|
9708
9738
|
tooltip,
|
|
9709
9739
|
trend: [],
|
|
9710
9740
|
progress: score,
|
|
@@ -9722,7 +9752,7 @@ function mentionShareTone(score) {
|
|
|
9722
9752
|
return "negative";
|
|
9723
9753
|
}
|
|
9724
9754
|
function describe(parts) {
|
|
9725
|
-
const { scope,
|
|
9755
|
+
const { scope, breakdown: breakdown2, branded } = parts;
|
|
9726
9756
|
const { projectMentionSnapshots, competitorMentionSnapshots, perCompetitor } = breakdown2;
|
|
9727
9757
|
const where = scope === "non-brand" ? "on non-brand queries" : "across your tracked queries";
|
|
9728
9758
|
const brandedNote = branded.snapshotsWithAnswerText > 0 && branded.score !== null ? ` Separately, you are named in ${branded.projectMentionSnapshots} of ${branded.snapshotsWithAnswerText} answers to queries that contain your name.` : "";
|
|
@@ -9734,10 +9764,11 @@ function describe(parts) {
|
|
|
9734
9764
|
}
|
|
9735
9765
|
const top = perCompetitor[0];
|
|
9736
9766
|
const total = projectMentionSnapshots + competitorMentionSnapshots;
|
|
9767
|
+
const share = formatPercent(projectMentionSnapshots / total);
|
|
9737
9768
|
if (!top) {
|
|
9738
|
-
return `${
|
|
9769
|
+
return `${share} of brand mentions ${where} are you (${projectMentionSnapshots} of ${total}).${brandedNote}`;
|
|
9739
9770
|
}
|
|
9740
|
-
return `${
|
|
9771
|
+
return `${share} of brand mentions ${where} are you. Top competitor: ${top.domain} (${top.mentionSnapshots} mentions).${brandedNote}`;
|
|
9741
9772
|
}
|
|
9742
9773
|
|
|
9743
9774
|
// ../intelligence/src/share-of-voice-frame.ts
|
|
@@ -9754,7 +9785,7 @@ function buildShareOfVoiceFrame(input) {
|
|
|
9754
9785
|
basis,
|
|
9755
9786
|
reason,
|
|
9756
9787
|
availability: reason === null ? "measured" : "not-measured",
|
|
9757
|
-
score: reason === null ?
|
|
9788
|
+
score: reason === null ? percentOf(input.projectMentions, denominator) : null,
|
|
9758
9789
|
denominator,
|
|
9759
9790
|
competitorMentions,
|
|
9760
9791
|
domains
|
|
@@ -10058,7 +10089,7 @@ function finalizeRow(row, answeredResults, mentionCredits, competitive) {
|
|
|
10058
10089
|
surfaceClass: row.surfaceClass,
|
|
10059
10090
|
pinned: row.pinned,
|
|
10060
10091
|
mentionCount: row.mentionCount,
|
|
10061
|
-
shareOfVoice: competitive ?
|
|
10092
|
+
shareOfVoice: competitive ? percentOf(row.mentionCount, mentionCredits) : null,
|
|
10062
10093
|
citationCount: row.citationCount,
|
|
10063
10094
|
answeredResults,
|
|
10064
10095
|
firstSeenAt: row.firstSeenAt,
|
|
@@ -10066,9 +10097,6 @@ function finalizeRow(row, answeredResults, mentionCredits, competitive) {
|
|
|
10066
10097
|
sampleUrls: [...row.sampleUrls]
|
|
10067
10098
|
};
|
|
10068
10099
|
}
|
|
10069
|
-
function roundPercentage(value) {
|
|
10070
|
-
return Math.round(value * 10) / 10;
|
|
10071
|
-
}
|
|
10072
10100
|
function compareCompetitiveRows(a, b) {
|
|
10073
10101
|
return b.mentionCount - a.mentionCount || b.citationCount - a.citationCount || a.domain.localeCompare(b.domain);
|
|
10074
10102
|
}
|
|
@@ -10097,7 +10125,7 @@ function toSection(tally) {
|
|
|
10097
10125
|
else if (ratio >= 0.2) pressureLabel = "Moderate";
|
|
10098
10126
|
else pressureLabel = "Low";
|
|
10099
10127
|
}
|
|
10100
|
-
const sharePct =
|
|
10128
|
+
const sharePct = percentOf(data.count, totalMentionedSlots);
|
|
10101
10129
|
return {
|
|
10102
10130
|
domain,
|
|
10103
10131
|
mentionCount: data.count,
|
|
@@ -10186,7 +10214,7 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
|
|
|
10186
10214
|
category,
|
|
10187
10215
|
label,
|
|
10188
10216
|
count: count4,
|
|
10189
|
-
sharePct:
|
|
10217
|
+
sharePct: percentOf(count4, totalCitations) ?? 0
|
|
10190
10218
|
})).sort((a, b) => b.count - a.count);
|
|
10191
10219
|
const topDomains = [...domainCounts.entries()].map(([domain, count4]) => ({
|
|
10192
10220
|
domain,
|
|
@@ -10378,13 +10406,13 @@ function buildVisibilityScore(snapshots, options) {
|
|
|
10378
10406
|
}
|
|
10379
10407
|
const totalCount = queryCited.size;
|
|
10380
10408
|
const citedCount = [...queryCited.values()].filter(Boolean).length;
|
|
10381
|
-
const score =
|
|
10409
|
+
const score = percentOf(citedCount, totalCount) ?? 0;
|
|
10382
10410
|
const runProviders = new Set(snapshots.map((s) => s.provider));
|
|
10383
10411
|
const runApiProviderCount = options.configuredApiProviders.filter((p) => runProviders.has(p)).length;
|
|
10384
10412
|
const isPartialProviderRun = options.configuredApiProviders.length > 1 && runApiProviderCount < options.configuredApiProviders.length;
|
|
10385
10413
|
return {
|
|
10386
10414
|
label: "Citation Coverage",
|
|
10387
|
-
value:
|
|
10415
|
+
value: formatPercent(citedCount / totalCount),
|
|
10388
10416
|
delta: `${citedCount} of ${totalCount} queries cited`,
|
|
10389
10417
|
tone: isPartialProviderRun ? "caution" : scoreTone(score),
|
|
10390
10418
|
description: `${citedCount} of ${totalCount} tracked queries found your domain in at least one AI answer engine.`,
|
|
@@ -10416,13 +10444,13 @@ function buildMentionCoverage(snapshots, options) {
|
|
|
10416
10444
|
}
|
|
10417
10445
|
const totalCount = queryMentioned.size;
|
|
10418
10446
|
const mentionedCount = [...queryMentioned.values()].filter(Boolean).length;
|
|
10419
|
-
const score =
|
|
10447
|
+
const score = percentOf(mentionedCount, totalCount) ?? 0;
|
|
10420
10448
|
const runProviders = new Set(snapshots.map((s) => s.provider));
|
|
10421
10449
|
const runApiProviderCount = options.configuredApiProviders.filter((p) => runProviders.has(p)).length;
|
|
10422
10450
|
const isPartialProviderRun = options.configuredApiProviders.length > 1 && runApiProviderCount < options.configuredApiProviders.length;
|
|
10423
10451
|
return {
|
|
10424
10452
|
label: "Mention Coverage",
|
|
10425
|
-
value:
|
|
10453
|
+
value: formatPercent(mentionedCount / totalCount),
|
|
10426
10454
|
delta: `${mentionedCount} of ${totalCount} queries mentioned`,
|
|
10427
10455
|
tone: isPartialProviderRun ? "caution" : scoreTone(score),
|
|
10428
10456
|
description: `${mentionedCount} of ${totalCount} tracked queries had your brand or domain in the AI answer text.`,
|
|
@@ -10468,7 +10496,7 @@ function buildGapQueryScore(snapshots) {
|
|
|
10468
10496
|
description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} currently cite competitors without citing your domain.` : "No competitive citation gaps detected in the latest visibility run.",
|
|
10469
10497
|
tooltip,
|
|
10470
10498
|
trend: [],
|
|
10471
|
-
progress:
|
|
10499
|
+
progress: percentOf(gapCount, totalCount) ?? 0
|
|
10472
10500
|
};
|
|
10473
10501
|
}
|
|
10474
10502
|
function buildMentionGapScore(snapshots) {
|
|
@@ -10505,7 +10533,7 @@ function buildMentionGapScore(snapshots) {
|
|
|
10505
10533
|
description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} mention competitors but never your brand.` : "No competitive mention gaps detected in the latest visibility run.",
|
|
10506
10534
|
tooltip,
|
|
10507
10535
|
trend: [],
|
|
10508
|
-
progress:
|
|
10536
|
+
progress: percentOf(gapCount, totalCount) ?? 0
|
|
10509
10537
|
};
|
|
10510
10538
|
}
|
|
10511
10539
|
|
|
@@ -10586,7 +10614,7 @@ function buildProviderScores(snapshots) {
|
|
|
10586
10614
|
).map(({ provider, model, cited, total }) => ({
|
|
10587
10615
|
provider,
|
|
10588
10616
|
model,
|
|
10589
|
-
score:
|
|
10617
|
+
score: percentOf(cited, total) ?? 0,
|
|
10590
10618
|
cited,
|
|
10591
10619
|
total
|
|
10592
10620
|
}));
|
|
@@ -10608,9 +10636,9 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
|
|
|
10608
10636
|
}
|
|
10609
10637
|
const totalCount = queryCited.size;
|
|
10610
10638
|
const citedCount = [...queryCited.values()].filter(Boolean).length;
|
|
10611
|
-
const citationRate =
|
|
10639
|
+
const citationRate = percentOf(citedCount, totalCount) ?? 0;
|
|
10612
10640
|
const mentionedCount = [...queryMentioned.values()].filter(Boolean).length;
|
|
10613
|
-
const mentionRate3 =
|
|
10641
|
+
const mentionRate3 = percentOf(mentionedCount, totalCount) ?? 0;
|
|
10614
10642
|
return {
|
|
10615
10643
|
runId: run.id,
|
|
10616
10644
|
createdAt: run.createdAt,
|
|
@@ -10642,7 +10670,8 @@ function buildProviderTrends(runs2, snapshotsByRunId, limit = 12) {
|
|
|
10642
10670
|
}
|
|
10643
10671
|
for (const key of keys) {
|
|
10644
10672
|
const queryMap = perKey.get(key);
|
|
10645
|
-
const
|
|
10673
|
+
const cited = queryMap ? [...queryMap.values()].filter(Boolean).length : 0;
|
|
10674
|
+
const rate2 = percentOf(cited, queryMap?.size ?? 0) ?? 0;
|
|
10646
10675
|
result.get(key).push({ rate: rate2, createdAt: run.createdAt });
|
|
10647
10676
|
}
|
|
10648
10677
|
}
|
|
@@ -10713,7 +10742,7 @@ function formatImpressions2(value) {
|
|
|
10713
10742
|
|
|
10714
10743
|
// ../intelligence/src/smoothed-delta.ts
|
|
10715
10744
|
var SMOOTHED_RUN_DELTA_MAX_WINDOW = 3;
|
|
10716
|
-
function smoothedRunDelta(points, valueFn, maxWindow = SMOOTHED_RUN_DELTA_MAX_WINDOW) {
|
|
10745
|
+
function smoothedRunDelta(points, valueFn, maxWindow = SMOOTHED_RUN_DELTA_MAX_WINDOW, ratioUnit) {
|
|
10717
10746
|
if (points.length < 2) return null;
|
|
10718
10747
|
const window = Math.min(maxWindow, Math.floor(points.length / 2));
|
|
10719
10748
|
const tail = points.slice(-window);
|
|
@@ -10721,8 +10750,9 @@ function smoothedRunDelta(points, valueFn, maxWindow = SMOOTHED_RUN_DELTA_MAX_WI
|
|
|
10721
10750
|
const sum = (arr) => arr.reduce((s, p) => s + valueFn(p), 0);
|
|
10722
10751
|
const currentAvg = sum(tail) / tail.length;
|
|
10723
10752
|
const priorAvg = sum(prior) / prior.length;
|
|
10724
|
-
const
|
|
10725
|
-
const
|
|
10753
|
+
const round = (value) => ratioUnit ? roundRatio(value, ratioUnit) : roundTo1Decimal(value);
|
|
10754
|
+
const current = round(currentAvg);
|
|
10755
|
+
const prior_ = round(priorAvg);
|
|
10726
10756
|
return {
|
|
10727
10757
|
current,
|
|
10728
10758
|
prior: prior_,
|
|
@@ -10819,7 +10849,7 @@ function analyzeGbp(signals) {
|
|
|
10819
10849
|
...base,
|
|
10820
10850
|
type: "gbp-metric-drop",
|
|
10821
10851
|
severity: abs >= GBP_METRIC_SEVERE_PCT ? "high" : "medium",
|
|
10822
|
-
title: `${loc.displayName}: ${metricLabel(worstMetric.metric)} down ${abs}
|
|
10852
|
+
title: `${loc.displayName}: ${metricLabel(worstMetric.metric)} down ${formatPercent(abs, RatioUnits.percent)} week-over-week`,
|
|
10823
10853
|
recommendation: {
|
|
10824
10854
|
action: "Investigate the local-visibility drop (profile changes, category edits, new competition)",
|
|
10825
10855
|
reason: `${metricLabel(worstMetric.metric)} fell from ${worstMetric.prior} to ${worstMetric.recent} (last 7d vs the prior 7d).`
|
|
@@ -10833,7 +10863,7 @@ function analyzeGbp(signals) {
|
|
|
10833
10863
|
...base,
|
|
10834
10864
|
type: "gbp-keyword-drop",
|
|
10835
10865
|
severity: worstKeyword.dropPct >= GBP_KEYWORD_SEVERE_PCT && !actionsHeldUp(loc) ? "high" : "medium",
|
|
10836
|
-
title: `${loc.displayName}: "${worstKeyword.keyword}" impressions down ${worstKeyword.dropPct}
|
|
10866
|
+
title: `${loc.displayName}: "${worstKeyword.keyword}" impressions down ${formatPercent(worstKeyword.dropPct, RatioUnits.percent)} month-over-month${window}`,
|
|
10837
10867
|
recommendation: {
|
|
10838
10868
|
action: "Check whether the property still ranks for this local search term and refresh the profile",
|
|
10839
10869
|
reason: `Search-keyword impressions for "${worstKeyword.keyword}" fell from ${worstKeyword.prior} to ${worstKeyword.recent} month-over-month.`
|
|
@@ -10867,7 +10897,7 @@ function pickWorstKeywordDrop(loc) {
|
|
|
10867
10897
|
if (point.recent == null || point.prior == null || point.prior < GBP_KEYWORD_MIN_BASELINE) continue;
|
|
10868
10898
|
const changeRatio = relativeChangeRatio(point.recent, point.prior);
|
|
10869
10899
|
if (changeRatio === null) continue;
|
|
10870
|
-
const dropPct =
|
|
10900
|
+
const dropPct = roundRatio(-changeRatio * 100, RatioUnits.percent);
|
|
10871
10901
|
if (dropPct < GBP_KEYWORD_DROP_PCT) continue;
|
|
10872
10902
|
if (!worst || dropPct > worst.dropPct) {
|
|
10873
10903
|
worst = { keyword: point.keyword, dropPct, recent: point.recent, prior: point.prior };
|
|
@@ -28627,6 +28657,7 @@ async function analyticsRoutes(app) {
|
|
|
28627
28657
|
byProvider: {},
|
|
28628
28658
|
trend: "stable",
|
|
28629
28659
|
mentionTrend: "stable",
|
|
28660
|
+
windowChange: { citationRate: null, mentionRate: null, mentionShare: null },
|
|
28630
28661
|
queryChanges: [],
|
|
28631
28662
|
basketChanges: [],
|
|
28632
28663
|
executionIdentityChanges: [],
|
|
@@ -28839,6 +28870,7 @@ async function analyticsRoutes(app) {
|
|
|
28839
28870
|
}
|
|
28840
28871
|
const trend = computeTrend(buckets, "citationRate");
|
|
28841
28872
|
const mentionTrend = computeTrend(buckets, "mentionRate");
|
|
28873
|
+
const windowChange = computeWindowChange(buckets);
|
|
28842
28874
|
const queryChanges = computeQueryChanges(projectQueries, cutoff);
|
|
28843
28875
|
const basketChanges = basketRevisions.slice(1).map((rev, i) => {
|
|
28844
28876
|
const previous = new Set(basketRevisions[i].members);
|
|
@@ -28860,7 +28892,7 @@ async function analyticsRoutes(app) {
|
|
|
28860
28892
|
}
|
|
28861
28893
|
previousExecutionChecksum = identity.checksum;
|
|
28862
28894
|
}
|
|
28863
|
-
return reply.send({ window, mentionShareScope, buckets, overall, byProvider, trend, mentionTrend, queryChanges, basketChanges, executionIdentityChanges, referenceBasketRevision: latestBasket?.revision ?? null, modelAttribution, servedModelAttribution, modelServiceMismatch, modelPointerChanges });
|
|
28895
|
+
return reply.send({ window, mentionShareScope, buckets, overall, byProvider, trend, mentionTrend, windowChange, queryChanges, basketChanges, executionIdentityChanges, referenceBasketRevision: latestBasket?.revision ?? null, modelAttribution, servedModelAttribution, modelServiceMismatch, modelPointerChanges });
|
|
28864
28896
|
});
|
|
28865
28897
|
app.get("/projects/:name/analytics/gaps", async (request, reply) => {
|
|
28866
28898
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -29328,7 +29360,7 @@ function computeMentionShareBucketMetric(snapshots, mentionShareCompetitors, cla
|
|
|
29328
29360
|
scope: result.scope,
|
|
29329
29361
|
// A project-only denominator is recognition evidence, not competitive
|
|
29330
29362
|
// share. Preserve the count but leave the rate undefined without a frame.
|
|
29331
|
-
rate: mentionShareCompetitors.length > 0 && denominator > 0 ?
|
|
29363
|
+
rate: mentionShareCompetitors.length > 0 && denominator > 0 ? roundRatio(projectMentionSnapshots / denominator, RatioUnits.fraction) : null,
|
|
29332
29364
|
projectMentionSnapshots,
|
|
29333
29365
|
competitorMentionSnapshots
|
|
29334
29366
|
};
|
|
@@ -29358,6 +29390,20 @@ function pooledRate(buckets, rateKey) {
|
|
|
29358
29390
|
}
|
|
29359
29391
|
return denominator > 0 ? numerator / denominator : 0;
|
|
29360
29392
|
}
|
|
29393
|
+
function windowRateChange(rates) {
|
|
29394
|
+
const first = rates[0];
|
|
29395
|
+
const latest = rates.at(-1);
|
|
29396
|
+
if (rates.length < 2 || first === void 0 || latest === void 0) return null;
|
|
29397
|
+
return { first, latest, delta: roundRatio(latest - first, RatioUnits.fraction) };
|
|
29398
|
+
}
|
|
29399
|
+
function computeWindowChange(buckets) {
|
|
29400
|
+
const measured = buckets.filter((b) => b.total > 0);
|
|
29401
|
+
return {
|
|
29402
|
+
citationRate: windowRateChange(measured.map((b) => b.citationRate)),
|
|
29403
|
+
mentionRate: windowRateChange(measured.map((b) => b.mentionRate)),
|
|
29404
|
+
mentionShare: windowRateChange(buckets.flatMap((b) => b.mentionShare.rate === null ? [] : [b.mentionShare.rate]))
|
|
29405
|
+
};
|
|
29406
|
+
}
|
|
29361
29407
|
function computeTrend(buckets, rateKey) {
|
|
29362
29408
|
const nonEmpty = buckets.filter((b) => b.total > 0);
|
|
29363
29409
|
if (nonEmpty.length < 2) return "stable";
|
|
@@ -29371,9 +29417,6 @@ function computeTrend(buckets, rateKey) {
|
|
|
29371
29417
|
if (diff < -0.05) return "declining";
|
|
29372
29418
|
return "stable";
|
|
29373
29419
|
}
|
|
29374
|
-
function round4(ratio) {
|
|
29375
|
-
return Math.round(ratio * 1e4) / 1e4;
|
|
29376
|
-
}
|
|
29377
29420
|
function bumpDomain(map, domain, category, label, surfaceClass) {
|
|
29378
29421
|
const existing = map.get(domain);
|
|
29379
29422
|
if (existing) existing.count += 1;
|
|
@@ -29388,8 +29431,8 @@ function buildRankedList(domains, limit, answerTotal, answersWithSources) {
|
|
|
29388
29431
|
const entries = shownEntries.map((d) => ({
|
|
29389
29432
|
domain: d.domain,
|
|
29390
29433
|
count: d.count,
|
|
29391
|
-
percentage: totalCitedSlots > 0 ?
|
|
29392
|
-
answerShare: answerTotal > 0 ?
|
|
29434
|
+
percentage: totalCitedSlots > 0 ? roundRatio(d.count / totalCitedSlots, RatioUnits.fraction) : 0,
|
|
29435
|
+
answerShare: answerTotal > 0 ? roundRatio(d.count / answerTotal, RatioUnits.fraction) : 0,
|
|
29393
29436
|
category: d.category,
|
|
29394
29437
|
label: d.label,
|
|
29395
29438
|
surfaceClass: d.surfaceClass
|
|
@@ -29406,7 +29449,7 @@ function buildRankedList(domains, limit, answerTotal, answersWithSources) {
|
|
|
29406
29449
|
surfaceClass,
|
|
29407
29450
|
label: surfaceClassLabel(surfaceClass),
|
|
29408
29451
|
count: v.count,
|
|
29409
|
-
percentage: totalCitedSlots > 0 ?
|
|
29452
|
+
percentage: totalCitedSlots > 0 ? roundRatio(v.count / totalCitedSlots, RatioUnits.fraction) : 0,
|
|
29410
29453
|
domainCount: v.domainCount
|
|
29411
29454
|
})).sort((a, b) => b.count - a.count || a.surfaceClass.localeCompare(b.surfaceClass));
|
|
29412
29455
|
return {
|
|
@@ -31610,7 +31653,7 @@ function renderExecutiveSummary(report) {
|
|
|
31610
31653
|
</div>
|
|
31611
31654
|
<div class="hero-proof">
|
|
31612
31655
|
<div class="mini-label">${copy.proofTiles.mentionCoverage}</div>
|
|
31613
|
-
<div class="mini-value">${s.mentionRate}
|
|
31656
|
+
<div class="mini-value">${formatPercent(s.mentionRate, RatioUnits.percent)}</div>
|
|
31614
31657
|
<div class="mini-copy">${escapeHtml2(headline.mentionedFragment)}</div>
|
|
31615
31658
|
</div>
|
|
31616
31659
|
<div class="hero-proof">
|
|
@@ -31623,12 +31666,12 @@ function renderExecutiveSummary(report) {
|
|
|
31623
31666
|
const metrics = [
|
|
31624
31667
|
{
|
|
31625
31668
|
label: copy.tiles.citationRate,
|
|
31626
|
-
value:
|
|
31669
|
+
value: formatPercent(s.citationRate, RatioUnits.percent),
|
|
31627
31670
|
delta: `<span class="tone-${headline.trendTone}">${headline.trendLabel}</span> \xB7 ${headline.citedFragment} \xB7 ${headline.providerCountLabel}`
|
|
31628
31671
|
},
|
|
31629
31672
|
{
|
|
31630
31673
|
label: copy.tiles.mentionRate,
|
|
31631
|
-
value:
|
|
31674
|
+
value: formatPercent(s.mentionRate, RatioUnits.percent),
|
|
31632
31675
|
delta: headline.mentionedFragment
|
|
31633
31676
|
},
|
|
31634
31677
|
{
|
|
@@ -31684,11 +31727,10 @@ function renderRateDeltaTile(label, delta, unit) {
|
|
|
31684
31727
|
if (!delta) {
|
|
31685
31728
|
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta">${REPORT_SECTION_COPY["whats-changed"].rateTileEmpty}</div></div>`;
|
|
31686
31729
|
}
|
|
31687
|
-
const valueSuffix = unit === "%" ? "%" : "";
|
|
31688
31730
|
const deltaText = reportRateDeltaCopy(delta, unit);
|
|
31689
31731
|
return `<div class="metric">
|
|
31690
31732
|
<div class="label">${escapeHtml2(label)}</div>
|
|
31691
|
-
<div class="value ${deltaToneClass(delta.direction)}">${delta
|
|
31733
|
+
<div class="value ${deltaToneClass(delta.direction)}">${reportRateDeltaValue(delta, unit)} <span style="font-size:14px;font-weight:500;">${reportDeltaArrow(delta.direction)}</span></div>
|
|
31692
31734
|
<div class="delta">${deltaText}</div>
|
|
31693
31735
|
</div>`;
|
|
31694
31736
|
}
|
|
@@ -31711,8 +31753,8 @@ function renderProviderMovements(movements, audience) {
|
|
|
31711
31753
|
const rows = meaningful.map((m) => {
|
|
31712
31754
|
return `<tr>
|
|
31713
31755
|
<td>${escapeHtml2(isClient ? reportProviderDisplayName(m.provider) : m.provider)}</td>
|
|
31714
|
-
<td class="numeric">${m.prior}
|
|
31715
|
-
<td class="numeric">${m.current}
|
|
31756
|
+
<td class="numeric">${formatPercent(m.prior, RatioUnits.percent)}</td>
|
|
31757
|
+
<td class="numeric">${formatPercent(m.current, RatioUnits.percent)}</td>
|
|
31716
31758
|
<td class="numeric ${deltaToneClass(m.direction)}">${reportMovementChangeCopy(m)}</td>
|
|
31717
31759
|
</tr>`;
|
|
31718
31760
|
}).join("");
|
|
@@ -31905,7 +31947,7 @@ function renderCompetitorLandscape(report) {
|
|
|
31905
31947
|
<td><span class="badge tone-${tone}">${escapeHtml2(c.pressureLabel)}</span></td>
|
|
31906
31948
|
<td class="numeric">${c.citationCount} / ${c.totalCount}</td>
|
|
31907
31949
|
<td class="numeric">${mentionCount} / ${mentionTotal}</td>
|
|
31908
|
-
<td class="numeric">${c.sharePct}
|
|
31950
|
+
<td class="numeric">${formatPercent(c.sharePct, RatioUnits.percent)}</td>
|
|
31909
31951
|
<td>${escapeHtml2(reportTruncatedList(c.citedQueries, 5))}${pagesDisclosure}</td>
|
|
31910
31952
|
</tr>`;
|
|
31911
31953
|
}).join("");
|
|
@@ -32016,7 +32058,7 @@ function renderAiSourceOrigin(report) {
|
|
|
32016
32058
|
`${headlineFragment}${table}${renderCategoryBars(origin.categories)}`
|
|
32017
32059
|
);
|
|
32018
32060
|
}
|
|
32019
|
-
function renderLineChart(points, color, title, height = 200) {
|
|
32061
|
+
function renderLineChart(points, color, title, height = 200, formatValue = formatNumber) {
|
|
32020
32062
|
if (points.length === 0) return "";
|
|
32021
32063
|
const width = 600;
|
|
32022
32064
|
const padX = 32;
|
|
@@ -32040,8 +32082,8 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
32040
32082
|
<h3>${escapeHtml2(title)}</h3>
|
|
32041
32083
|
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml2(reportLineChartLabel(title))}">
|
|
32042
32084
|
<line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
|
|
32043
|
-
<text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${
|
|
32044
|
-
<text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end"
|
|
32085
|
+
<text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatValue(max2)}</text>
|
|
32086
|
+
<text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatValue(0)}</text>
|
|
32045
32087
|
<path d="${path8}" stroke="${color}" stroke-width="2" fill="none" />
|
|
32046
32088
|
${dots}
|
|
32047
32089
|
${xLabels}
|
|
@@ -32062,7 +32104,7 @@ function renderGsc(report) {
|
|
|
32062
32104
|
<td>${escapeHtml2(q.query)}</td>
|
|
32063
32105
|
<td class="numeric">${formatNumber(q.clicks)}</td>
|
|
32064
32106
|
<td class="numeric">${formatNumber(q.impressions)}</td>
|
|
32065
|
-
<td class="numeric">${
|
|
32107
|
+
<td class="numeric">${formatPercent(q.ctr)}</td>
|
|
32066
32108
|
<td class="numeric">${q.avgPosition.toFixed(1)}</td>
|
|
32067
32109
|
<td><span class="badge tone-neutral">${escapeHtml2(q.category)}</span></td>
|
|
32068
32110
|
</tr>`).join("");
|
|
@@ -32100,7 +32142,7 @@ function renderGsc(report) {
|
|
|
32100
32142
|
`<div class="metric-grid">
|
|
32101
32143
|
<div class="metric"><div class="label">${copy.tiles.clicks}</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
|
|
32102
32144
|
<div class="metric"><div class="label">${copy.tiles.impressions}</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
|
|
32103
|
-
<div class="metric"><div class="label">${copy.tiles.ctr}</div><div class="value">${
|
|
32145
|
+
<div class="metric"><div class="label">${copy.tiles.ctr}</div><div class="value">${formatPercent(gsc.ctr)}</div></div>
|
|
32104
32146
|
<div class="metric"><div class="label">${copy.tiles.position}</div><div class="value">${gsc.avgPosition.toFixed(1)}</div></div>
|
|
32105
32147
|
</div>
|
|
32106
32148
|
${trendChart}
|
|
@@ -32448,7 +32490,7 @@ function renderIndexingHealth(report) {
|
|
|
32448
32490
|
`<div class="metric-grid">
|
|
32449
32491
|
<div class="metric"><div class="label">${copy.tiles.indexed}</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
|
|
32450
32492
|
<div class="metric"><div class="label">${copy.tiles.total}</div><div class="value">${formatNumber(ih.total)}</div></div>
|
|
32451
|
-
<div class="metric"><div class="label">${copy.tiles.share}</div><div class="value">${ih.indexedPct}
|
|
32493
|
+
<div class="metric"><div class="label">${copy.tiles.share}</div><div class="value">${formatPercent(ih.indexedPct, RatioUnits.percent)}</div></div>
|
|
32452
32494
|
</div>
|
|
32453
32495
|
<div class="chart-card">
|
|
32454
32496
|
<h3>${copy.coverageHeading}</h3>
|
|
@@ -32476,12 +32518,13 @@ function renderCitationsTrend(report) {
|
|
|
32476
32518
|
trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
|
|
32477
32519
|
COLORS.positive,
|
|
32478
32520
|
copy.chartTitle,
|
|
32479
|
-
220
|
|
32521
|
+
220,
|
|
32522
|
+
(value) => formatPercent(value, RatioUnits.percent)
|
|
32480
32523
|
);
|
|
32481
32524
|
const rows = trend.map((t) => `
|
|
32482
32525
|
<tr>
|
|
32483
32526
|
<td>${formatDate(t.date)}</td>
|
|
32484
|
-
<td class="numeric">${t.citationRate}
|
|
32527
|
+
<td class="numeric">${formatPercent(t.citationRate, RatioUnits.percent)} <span class="cell-pending">(${t.citedQueryCount}/${t.totalQueryCount})</span></td>
|
|
32485
32528
|
<td>${escapeHtml2(reportTrendProviderRates(t.providerRates))}</td>
|
|
32486
32529
|
</tr>`).join("");
|
|
32487
32530
|
const [checkHeader, citedQueriesHeader, engineRatesHeader] = copy.breakdownHeaders;
|
|
@@ -32668,7 +32711,7 @@ function renderClientSummary(report) {
|
|
|
32668
32711
|
const sc = report.citationScorecard;
|
|
32669
32712
|
const copy = REPORT_SECTION_COPY["client-summary"];
|
|
32670
32713
|
const totalQ = s.totalQueryCount ?? 0;
|
|
32671
|
-
const heroNumber = totalQ > 0 ?
|
|
32714
|
+
const heroNumber = totalQ > 0 ? formatPercent(s.mentionRate, RatioUnits.percent) : "\u2014";
|
|
32672
32715
|
const heroSentence = reportClientHeroSentence(totalQ, s.mentionedQueryCount);
|
|
32673
32716
|
const trend = reportClientTrendCopy(report.whatsChanged.mentionRate);
|
|
32674
32717
|
const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml2(trend.text)}</p>` : "";
|
|
@@ -32682,12 +32725,12 @@ function renderClientSummary(report) {
|
|
|
32682
32725
|
const tiles = `<div class="client-metric-grid">
|
|
32683
32726
|
<div class="client-metric-tile">
|
|
32684
32727
|
<div class="label">${copy.tiles.mentioned}</div>
|
|
32685
|
-
<div class="value">${s.mentionRate}
|
|
32728
|
+
<div class="value">${formatPercent(s.mentionRate, RatioUnits.percent)}</div>
|
|
32686
32729
|
<div class="subtitle">${reportClientMentionedSubtitle(s.mentionedQueryCount, totalQ)}</div>
|
|
32687
32730
|
</div>
|
|
32688
32731
|
<div class="client-metric-tile">
|
|
32689
32732
|
<div class="label">${copy.tiles.cited}</div>
|
|
32690
|
-
<div class="value">${s.citationRate}
|
|
32733
|
+
<div class="value">${formatPercent(s.citationRate, RatioUnits.percent)}</div>
|
|
32691
32734
|
<div class="subtitle">${reportClientCitedSubtitle(s.citedQueryCount, totalQ)}</div>
|
|
32692
32735
|
</div>
|
|
32693
32736
|
<div class="client-metric-tile">
|
|
@@ -32719,7 +32762,7 @@ function renderClientSummary(report) {
|
|
|
32719
32762
|
return `<div class="client-bar-row">
|
|
32720
32763
|
<span class="bar-label">${escapeHtml2(reportProviderDisplayName(r.provider))}</span>
|
|
32721
32764
|
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
|
32722
|
-
<span class="bar-value">${r.mentionRate}
|
|
32765
|
+
<span class="bar-value">${formatPercent(r.mentionRate, RatioUnits.percent)} <span class="bar-value-sub">(${r.mentionedCount}/${r.totalCount})</span></span>
|
|
32723
32766
|
</div>`;
|
|
32724
32767
|
}).join("")}
|
|
32725
32768
|
</div>
|
|
@@ -32759,7 +32802,7 @@ function renderClientEvidenceSummary(report) {
|
|
|
32759
32802
|
cards.push(`<div class="client-card">
|
|
32760
32803
|
<h3>${copy.indexing.heading}</h3>
|
|
32761
32804
|
<p class="card-subtitle">${copy.indexing.subtitle}</p>
|
|
32762
|
-
<div class="client-progress-number tone-${tone}">${indexing.indexedPct}
|
|
32805
|
+
<div class="client-progress-number tone-${tone}">${formatPercent(indexing.indexedPct, RatioUnits.percent)}</div>
|
|
32763
32806
|
<div style="font-size:12px;color:${COLORS.textMuted};">${reportClientIndexedPages(indexing.indexed, indexing.total)}</div>
|
|
32764
32807
|
<div class="client-progress-bar"><div class="client-progress-fill tone-${tone}" style="width:${fillPct}%"></div></div>
|
|
32765
32808
|
<p style="margin:0;font-size:12px;color:${COLORS.textMuted};"><strong style="color:${COLORS.text};">${formatNumber(indexing.notIndexed)}</strong> ${reportClientNotIndexedTail(indexing.notIndexed)}</p>
|
|
@@ -33180,7 +33223,7 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
33180
33223
|
category,
|
|
33181
33224
|
clicks: agg.clicks,
|
|
33182
33225
|
impressions: agg.impressions,
|
|
33183
|
-
sharePct:
|
|
33226
|
+
sharePct: percentOf(agg.clicks, categoryTotalClicks) ?? 0
|
|
33184
33227
|
})).sort((a, b) => b.clicks - a.clicks);
|
|
33185
33228
|
const periodStart = trend[0]?.date ?? "";
|
|
33186
33229
|
const periodEnd = trend.at(-1)?.date ?? "";
|
|
@@ -33284,7 +33327,7 @@ function buildGaSection(db, projectId, windowDays2) {
|
|
|
33284
33327
|
channelBreakdown.push({
|
|
33285
33328
|
channel: b.channel,
|
|
33286
33329
|
sessions: b.sessions,
|
|
33287
|
-
sharePct:
|
|
33330
|
+
sharePct: percentOf(b.sessions, totalSessions) ?? 0
|
|
33288
33331
|
});
|
|
33289
33332
|
}
|
|
33290
33333
|
}
|
|
@@ -33327,7 +33370,7 @@ function buildSocialReferrals(db, projectId, windowDays2) {
|
|
|
33327
33370
|
const channels = [...channelAgg.entries()].map(([channelGroup, sessions]) => ({
|
|
33328
33371
|
channelGroup,
|
|
33329
33372
|
sessions,
|
|
33330
|
-
sharePct:
|
|
33373
|
+
sharePct: percentOf(sessions, total) ?? 0
|
|
33331
33374
|
})).sort((a, b) => b.sessions - a.sessions);
|
|
33332
33375
|
const topCampaigns = [...campaignAgg.values()].sort((a, b) => b.sessions - a.sessions).slice(0, TOP_CAMPAIGN_LIMIT);
|
|
33333
33376
|
return {
|
|
@@ -33409,7 +33452,7 @@ function buildAiReferrals(db, projectId, windowDays2) {
|
|
|
33409
33452
|
sessions: data.sessions,
|
|
33410
33453
|
paidSessions: data.paidSessions,
|
|
33411
33454
|
organicSessions: data.organicSessions,
|
|
33412
|
-
sharePct:
|
|
33455
|
+
sharePct: percentOf(data.sessions, total) ?? 0
|
|
33413
33456
|
})).sort((a, b) => b.sessions - a.sessions);
|
|
33414
33457
|
const trend = [...trendAgg.entries()].map(([date, sessions]) => ({ date, sessions })).sort((a, b) => a.date.localeCompare(b.date));
|
|
33415
33458
|
const topLandingPages = [...pageAgg.entries()].map(([page3, data]) => ({ page: page3, sessions: data.sessions })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
|
|
@@ -33740,7 +33783,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
33740
33783
|
notIndexed: gsc.notIndexed,
|
|
33741
33784
|
deindexed: 0,
|
|
33742
33785
|
unknown: 0,
|
|
33743
|
-
indexedPct:
|
|
33786
|
+
indexedPct: percentOf(gsc.indexed, total) ?? 0
|
|
33744
33787
|
};
|
|
33745
33788
|
}
|
|
33746
33789
|
const bing = db.select().from(bingCoverageSnapshots).where(eq37(bingCoverageSnapshots.projectId, projectId)).orderBy(desc17(bingCoverageSnapshots.date)).limit(1).get();
|
|
@@ -33753,7 +33796,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
33753
33796
|
notIndexed: bing.notIndexed,
|
|
33754
33797
|
deindexed: 0,
|
|
33755
33798
|
unknown: bing.unknown,
|
|
33756
|
-
indexedPct:
|
|
33799
|
+
indexedPct: percentOf(bing.indexed, total) ?? 0
|
|
33757
33800
|
};
|
|
33758
33801
|
}
|
|
33759
33802
|
return null;
|
|
@@ -33784,12 +33827,12 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
33784
33827
|
const totalQueries = new Set(snaps.map((snapshot) => snapshot.queryId)).size;
|
|
33785
33828
|
const citedQueryCount = citedQueryIds.size;
|
|
33786
33829
|
const mentionedQueryCount = mentionedQueryIds.size;
|
|
33787
|
-
const citationRate =
|
|
33788
|
-
const mentionRate3 =
|
|
33830
|
+
const citationRate = percentOf(citedQueryCount, totalQueries) ?? 0;
|
|
33831
|
+
const mentionRate3 = percentOf(mentionedQueryCount, totalQueries) ?? 0;
|
|
33789
33832
|
const providerRates = [...providerCounts.entries()].map(([provider, counts2]) => ({
|
|
33790
33833
|
provider,
|
|
33791
|
-
citationRate:
|
|
33792
|
-
mentionRate:
|
|
33834
|
+
citationRate: percentOf(counts2.cited, counts2.total) ?? 0,
|
|
33835
|
+
mentionRate: percentOf(counts2.mentioned, counts2.total) ?? 0
|
|
33793
33836
|
})).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
33794
33837
|
points.push({
|
|
33795
33838
|
runId: run.id,
|
|
@@ -33911,7 +33954,7 @@ function buildExecutiveFindings(citationRate, citedQueryCount, totalQueryCount,
|
|
|
33911
33954
|
const queryNoun = totalQueryCount === 1 ? "query" : "queries";
|
|
33912
33955
|
const ratioFragment = totalQueryCount > 0 ? ` (${citedQueryCount} of ${totalQueryCount} ${queryNoun} cited)` : "";
|
|
33913
33956
|
findings.push({
|
|
33914
|
-
title: `Citation rate at ${citationRate}
|
|
33957
|
+
title: `Citation rate at ${formatPercent(citationRate, RatioUnits.percent)}${ratioFragment}`,
|
|
33915
33958
|
detail,
|
|
33916
33959
|
tone
|
|
33917
33960
|
});
|
|
@@ -34044,7 +34087,7 @@ function buildReportActionPlan(input) {
|
|
|
34044
34087
|
if (input.indexingHealth && input.indexingHealth.total > 0 && input.indexingHealth.indexedPct < 70) {
|
|
34045
34088
|
const ih = input.indexingHealth;
|
|
34046
34089
|
const evidence = [
|
|
34047
|
-
`${ih.indexedPct}
|
|
34090
|
+
`${formatPercent(ih.indexedPct, RatioUnits.percent)} indexed (${ih.indexed}/${ih.total})`,
|
|
34048
34091
|
`${ih.notIndexed} not indexed${ih.deindexed > 0 ? `, ${ih.deindexed} deindexed` : ""}`
|
|
34049
34092
|
];
|
|
34050
34093
|
actions.push({
|
|
@@ -34119,7 +34162,7 @@ function buildReportActionPlan(input) {
|
|
|
34119
34162
|
"They are stronger evidence than a generic topic list because the model is already retrieving competing content."
|
|
34120
34163
|
],
|
|
34121
34164
|
evidence: [
|
|
34122
|
-
`"${topGap.query}" missed at ${
|
|
34165
|
+
`"${topGap.query}" missed at ${formatPercent(topGap.missRate)} with ${topGap.competitorCount} competitor${topGap.competitorCount === 1 ? "" : "s"} cited`,
|
|
34123
34166
|
`Cited competitors: ${reportCompactList(topGap.competitorDomains)}`
|
|
34124
34167
|
],
|
|
34125
34168
|
successMetric: "The top content-gap query moves from missed to cited or mentioned after the recommended content work ships.",
|
|
@@ -34186,7 +34229,7 @@ function buildClientSummary(reportLike) {
|
|
|
34186
34229
|
const s = reportLike.executiveSummary;
|
|
34187
34230
|
const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
|
|
34188
34231
|
const headline = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} mention the brand in AI answers` : "No tracked queries have completed a check yet";
|
|
34189
|
-
const overview = (s.totalQueryCount ?? 0) > 0 ? `${reportLike.canonicalDomain} is mentioned on ${s.mentionRate}
|
|
34232
|
+
const overview = (s.totalQueryCount ?? 0) > 0 ? `${reportLike.canonicalDomain} is mentioned on ${formatPercent(s.mentionRate, RatioUnits.percent)} of tracked queries and cited on ${formatPercent(s.citationRate, RatioUnits.percent)} of tracked queries. ${mentionTrendSentence(reportLike.whatsChanged.mentionRate)}` : "At least one completed check is needed before this can summarize how the brand appears in AI answers.";
|
|
34190
34233
|
const confidenceNotes = [];
|
|
34191
34234
|
if (s.totalQueryCount === 0) {
|
|
34192
34235
|
confidenceNotes.push("Confidence is low until the first tracked query check completes.");
|
|
@@ -34240,7 +34283,7 @@ function buildAgencyDiagnostics(input) {
|
|
|
34240
34283
|
const provider = input.indexingHealth.provider ?? "the connected provider";
|
|
34241
34284
|
diagnostics.push({
|
|
34242
34285
|
title: "Indexing health",
|
|
34243
|
-
detail: inspected ? `${input.indexingHealth.indexedPct}
|
|
34286
|
+
detail: inspected ? `${formatPercent(input.indexingHealth.indexedPct, RatioUnits.percent)} of inspected URLs are indexed in ${provider}.` : `No URLs have been inspected in ${provider} yet, so indexing coverage is not measured.`,
|
|
34244
34287
|
// Not measured is not a finding, so it must not carry a finding's tone.
|
|
34245
34288
|
severity: !inspected ? "neutral" : input.indexingHealth.indexedPct >= 90 ? "positive" : input.indexingHealth.indexedPct >= 70 ? "caution" : "negative",
|
|
34246
34289
|
evidence: inspected ? [
|
|
@@ -34292,7 +34335,7 @@ function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHisto
|
|
|
34292
34335
|
const arrow = citation.direction === "up" ? "\u2191" : citation.direction === "down" ? "\u2193" : "\u2192";
|
|
34293
34336
|
const verb = citation.direction === "up" ? "rose" : citation.direction === "down" ? "fell" : "held";
|
|
34294
34337
|
const smoothingHint = citation.window && citation.window >= 2 ? ` (avg of last ${citation.window} checks)` : "";
|
|
34295
|
-
parts.push(`Citation rate ${verb} ${citation.prior}
|
|
34338
|
+
parts.push(`Citation rate ${verb} ${formatPercent(citation.prior, RatioUnits.percent)} ${arrow} ${formatPercent(citation.current, RatioUnits.percent)}${smoothingHint}`);
|
|
34296
34339
|
}
|
|
34297
34340
|
if (aiReferrals && aiReferrals.direction !== "flat") {
|
|
34298
34341
|
const arrow = aiReferrals.direction === "up" ? "\u2191" : "\u2193";
|
|
@@ -34309,12 +34352,12 @@ function buildWhatsChanged(input) {
|
|
|
34309
34352
|
const latest = citationsTrend.at(-1);
|
|
34310
34353
|
const prior = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
|
|
34311
34354
|
const enoughHistory = !baseline && latest !== void 0 && prior !== void 0;
|
|
34312
|
-
const citationRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.citationRate);
|
|
34355
|
+
const citationRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.citationRate, SMOOTHED_RUN_DELTA_MAX_WINDOW, RatioUnits.percent);
|
|
34313
34356
|
const citationRate = enoughHistory && citationRateSmoothed ? {
|
|
34314
34357
|
...citationRateSmoothed,
|
|
34315
34358
|
direction: rateDirection(citationRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
|
|
34316
34359
|
} : null;
|
|
34317
|
-
const mentionRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.mentionRate);
|
|
34360
|
+
const mentionRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.mentionRate, SMOOTHED_RUN_DELTA_MAX_WINDOW, RatioUnits.percent);
|
|
34318
34361
|
const mentionRate3 = enoughHistory && mentionRateSmoothed ? {
|
|
34319
34362
|
...mentionRateSmoothed,
|
|
34320
34363
|
direction: rateDirection(mentionRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
|
|
@@ -34335,7 +34378,7 @@ function buildWhatsChanged(input) {
|
|
|
34335
34378
|
for (const cur of latest.providerRates) {
|
|
34336
34379
|
const priorRate = priorByProvider.get(cur.provider);
|
|
34337
34380
|
if (priorRate === void 0) continue;
|
|
34338
|
-
const deltaAbs = cur.citationRate - priorRate;
|
|
34381
|
+
const deltaAbs = roundRatio(cur.citationRate - priorRate, RatioUnits.percent);
|
|
34339
34382
|
providerMovements.push({
|
|
34340
34383
|
provider: cur.provider,
|
|
34341
34384
|
current: cur.citationRate,
|
|
@@ -34448,7 +34491,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
34448
34491
|
pressureLabel: row.mentions === 0 ? "None" : row.mentions / share.snapshotsWithAnswerText >= 0.5 ? "High" : row.mentions / share.snapshotsWithAnswerText >= 0.2 ? "Moderate" : "Low",
|
|
34449
34492
|
mentionCount: counts2.get(row.domain) ?? 0,
|
|
34450
34493
|
totalCount: share.snapshotsWithAnswerText,
|
|
34451
|
-
sharePct: share.availability === "measured" ?
|
|
34494
|
+
sharePct: share.availability === "measured" ? percentOf(counts2.get(row.domain) ?? 0, share.projectMentions + share.competitorMentions) : null
|
|
34452
34495
|
}));
|
|
34453
34496
|
}
|
|
34454
34497
|
Object.assign(mentionLandscape, mentionLandscape.nonBrand);
|
|
@@ -34498,8 +34541,8 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
34498
34541
|
}
|
|
34499
34542
|
const citedQueryCount = citedQueryIds.size;
|
|
34500
34543
|
const mentionedQueryCount = mentionedQueryIds.size;
|
|
34501
|
-
const citationRate =
|
|
34502
|
-
const mentionRate3 =
|
|
34544
|
+
const citationRate = percentOf(citedQueryCount, totalQueryCount) ?? 0;
|
|
34545
|
+
const mentionRate3 = percentOf(mentionedQueryCount, totalQueryCount) ?? 0;
|
|
34503
34546
|
const trendBaseline = isTrendBaseline(citationsTrend);
|
|
34504
34547
|
const latestPoint = citationsTrend.at(-1);
|
|
34505
34548
|
const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
|
|
@@ -35333,7 +35376,7 @@ function summarizeServer(crawlers, fetches, referrals) {
|
|
|
35333
35376
|
function comparisonDetail(latest, prior) {
|
|
35334
35377
|
if (prior === 0) return `${latest} in the latest cohort versus 0 in the prior cohort`;
|
|
35335
35378
|
const change = deltaPercent(latest, prior) ?? 0;
|
|
35336
|
-
return `${latest} in the latest cohort versus ${prior} prior (${change
|
|
35379
|
+
return `${latest} in the latest cohort versus ${prior} prior (${formatSignedPercent(change, RatioUnits.percent)})`;
|
|
35337
35380
|
}
|
|
35338
35381
|
function aggregateCoverage(row) {
|
|
35339
35382
|
return row?.startDate && row.endDate ? { startDate: row.startDate, endDate: row.endDate, observedDays: Number(row.observedDays) } : null;
|
|
@@ -36250,9 +36293,6 @@ function computeVisibilityCompare(input) {
|
|
|
36250
36293
|
}
|
|
36251
36294
|
|
|
36252
36295
|
// ../api-routes/src/visibility-stats.ts
|
|
36253
|
-
function round42(value) {
|
|
36254
|
-
return Math.round(value * 1e4) / 1e4;
|
|
36255
|
-
}
|
|
36256
36296
|
function emptyAgg() {
|
|
36257
36297
|
return { total: 0, checked: 0, mentioned: 0, cited: 0, first: null, last: null };
|
|
36258
36298
|
}
|
|
@@ -36272,8 +36312,8 @@ function counts(agg) {
|
|
|
36272
36312
|
cited: agg.cited,
|
|
36273
36313
|
// mention proportion is over the CHECKED sample; citation proportion is
|
|
36274
36314
|
// over the full total (every snapshot is checked for citation).
|
|
36275
|
-
mentionRate: agg.checked > 0 ?
|
|
36276
|
-
citedRate: agg.total > 0 ?
|
|
36315
|
+
mentionRate: agg.checked > 0 ? roundRatio(agg.mentioned / agg.checked, RatioUnits.fraction) : null,
|
|
36316
|
+
citedRate: agg.total > 0 ? roundRatio(agg.cited / agg.total, RatioUnits.fraction) : null
|
|
36277
36317
|
};
|
|
36278
36318
|
}
|
|
36279
36319
|
function providerEntries(byProvider) {
|
|
@@ -37367,13 +37407,13 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
37367
37407
|
const deindexedLabel = deindexed === 1 ? "URL" : "URLs";
|
|
37368
37408
|
return {
|
|
37369
37409
|
label: "Index Coverage",
|
|
37370
|
-
value:
|
|
37410
|
+
value: formatPercent(chosen.indexed / total),
|
|
37371
37411
|
delta: `${chosen.provider} \xB7 ${chosen.indexed} of ${total} indexed`,
|
|
37372
37412
|
tone,
|
|
37373
37413
|
description: deindexed > 0 ? `${deindexed} deindexed ${deindexedLabel} detected in the latest Google Search Console inspection.` : `${chosen.notIndexed} ${notIndexedLabel} not indexed in ${chosen.provider === "Google" ? "Google Search Console" : "Bing Webmaster Tools"}.`,
|
|
37374
37414
|
tooltip,
|
|
37375
37415
|
trend: [],
|
|
37376
|
-
progress:
|
|
37416
|
+
progress: roundRatio(percentage, RatioUnits.percent)
|
|
37377
37417
|
};
|
|
37378
37418
|
}
|
|
37379
37419
|
function countGoogleDeindexedUrls(app, projectId) {
|
|
@@ -37726,6 +37766,9 @@ var SCHEMA_TABLE = {
|
|
|
37726
37766
|
GA4PropertiesDto: ga4PropertiesDtoSchema,
|
|
37727
37767
|
GA4StatusDto: ga4StatusDtoSchema,
|
|
37728
37768
|
GA4SyncResponseDto: ga4SyncResponseDtoSchema,
|
|
37769
|
+
GaAttributionTrendResponse: gaAttributionTrendResponseSchema,
|
|
37770
|
+
GaSocialReferralTrendResponse: gaSocialReferralTrendResponseSchema,
|
|
37771
|
+
GaTrafficResponse: gaTrafficResponseSchema,
|
|
37729
37772
|
GbpAccountListResponse: gbpAccountListResponseSchema,
|
|
37730
37773
|
GbpDailyMetricListResponse: gbpDailyMetricListResponseSchema,
|
|
37731
37774
|
GbpKeywordImpressionListResponse: gbpKeywordImpressionListResponseSchema,
|
|
@@ -37983,6 +38026,7 @@ function errorResponse(description) {
|
|
|
37983
38026
|
}
|
|
37984
38027
|
|
|
37985
38028
|
// ../api-routes/src/openapi.ts
|
|
38029
|
+
var GA_MOVER_DESCRIPTION = `A biggest mover is the source whose sessions changed most, in either direction, over the last 7 days against the 7 before. A source with no sessions in the prior 7 days has changeBasis new and a null changePct: a change from zero has no percentage. Below ${MIN_PCT_BASE} prior sessions changeBasis is small-base and changeSessions is the figure to state.`;
|
|
37986
38030
|
var stringSchema = { type: "string" };
|
|
37987
38031
|
var booleanSchema = { type: "boolean" };
|
|
37988
38032
|
var integerSchema = { type: "integer" };
|
|
@@ -43556,11 +43600,11 @@ var routeCatalog = [
|
|
|
43556
43600
|
method: "get",
|
|
43557
43601
|
path: "/api/v1/projects/{name}/ga/traffic",
|
|
43558
43602
|
summary: "Get GA4 landing page traffic, channel breakdown, and AI referral landing pages",
|
|
43603
|
+
description: "Every figure is measured over one window (windowStart to windowEnd). Each AI and social referral row carries share, its fraction of that table's sessions, so a table's shares add up to 1; each top page carries organicShare, its organic sessions over its own sessions.",
|
|
43559
43604
|
tags: ["ga4"],
|
|
43560
43605
|
parameters: [nameParameter, limitQueryParameter, analyticsWindowParameter, analyticsStartDateParameter, analyticsEndDateParameter],
|
|
43561
43606
|
responses: {
|
|
43562
|
-
|
|
43563
|
-
200: rawJsonResponse("GA4 traffic data returned.", looseObjectSchema),
|
|
43607
|
+
200: jsonResponse("GA4 traffic data returned.", "GaTrafficResponse"),
|
|
43564
43608
|
400: errorResponse("GA4 is not connected."),
|
|
43565
43609
|
404: errorResponse("Project not found.")
|
|
43566
43610
|
}
|
|
@@ -43607,11 +43651,11 @@ var routeCatalog = [
|
|
|
43607
43651
|
method: "get",
|
|
43608
43652
|
path: "/api/v1/projects/{name}/ga/social-referral-trend",
|
|
43609
43653
|
summary: "Get social referral trend (7d/30d) with biggest mover",
|
|
43654
|
+
description: GA_MOVER_DESCRIPTION,
|
|
43610
43655
|
tags: ["ga4"],
|
|
43611
43656
|
parameters: [nameParameter],
|
|
43612
43657
|
responses: {
|
|
43613
|
-
|
|
43614
|
-
200: rawJsonResponse("Social referral trend returned.", looseObjectSchema),
|
|
43658
|
+
200: jsonResponse("Social referral trend returned.", "GaSocialReferralTrendResponse"),
|
|
43615
43659
|
400: errorResponse("GA4 is not connected."),
|
|
43616
43660
|
404: errorResponse("Project not found.")
|
|
43617
43661
|
}
|
|
@@ -43620,11 +43664,11 @@ var routeCatalog = [
|
|
|
43620
43664
|
method: "get",
|
|
43621
43665
|
path: "/api/v1/projects/{name}/ga/attribution-trend",
|
|
43622
43666
|
summary: "Get per-channel attribution trends (7d/30d) for organic, AI, and social",
|
|
43667
|
+
description: GA_MOVER_DESCRIPTION,
|
|
43623
43668
|
tags: ["ga4"],
|
|
43624
43669
|
parameters: [nameParameter],
|
|
43625
43670
|
responses: {
|
|
43626
|
-
|
|
43627
|
-
200: rawJsonResponse("Attribution trend returned.", looseObjectSchema),
|
|
43671
|
+
200: jsonResponse("Attribution trend returned.", "GaAttributionTrendResponse"),
|
|
43628
43672
|
400: errorResponse("GA4 is not connected."),
|
|
43629
43673
|
404: errorResponse("Project not found.")
|
|
43630
43674
|
}
|
|
@@ -46426,10 +46470,6 @@ function daysBetween(from, to) {
|
|
|
46426
46470
|
const [ty, tm, td] = to.split("-").map(Number);
|
|
46427
46471
|
return Math.round((Date.UTC(ty, tm - 1, td) - Date.UTC(fy, fm - 1, fd)) / 864e5);
|
|
46428
46472
|
}
|
|
46429
|
-
function roundPct(numerator, denominator) {
|
|
46430
|
-
if (denominator === 0) return 0;
|
|
46431
|
-
return Math.round(numerator / denominator * 100);
|
|
46432
|
-
}
|
|
46433
46473
|
function computeWindowDelta(rows, referenceDate) {
|
|
46434
46474
|
const recentLow = shiftDate(referenceDate, -7);
|
|
46435
46475
|
const priorLow = shiftDate(referenceDate, -14);
|
|
@@ -46494,7 +46534,7 @@ function buildTimeseries(rows, freshness, windowDays2 = 30) {
|
|
|
46494
46534
|
function computeKeywordCoverage(rows) {
|
|
46495
46535
|
const total = rows.length;
|
|
46496
46536
|
const thresholdedCount = rows.filter((r) => r.valueCount === null).length;
|
|
46497
|
-
return { total, thresholdedCount, thresholdedPct:
|
|
46537
|
+
return { total, thresholdedCount, thresholdedPct: percentOf(thresholdedCount, total) ?? 0 };
|
|
46498
46538
|
}
|
|
46499
46539
|
function summarizePlaceActions(rows) {
|
|
46500
46540
|
return {
|
|
@@ -49269,7 +49309,12 @@ async function googleRoutes(app, opts) {
|
|
|
49269
49309
|
indexed,
|
|
49270
49310
|
notIndexed,
|
|
49271
49311
|
deindexed: deindexedUrls.length,
|
|
49272
|
-
percentage:
|
|
49312
|
+
percentage: percentOf(indexed, total) ?? 0,
|
|
49313
|
+
// The coverage donut's two arcs and the CLI summary. Every latest
|
|
49314
|
+
// inspection lands in exactly one bucket, so the two shares sum to 1.
|
|
49315
|
+
// Unrounded, so the two arcs add up to 1.
|
|
49316
|
+
indexedShare: total > 0 ? indexed / total : null,
|
|
49317
|
+
notIndexedShare: total > 0 ? notIndexed / total : null
|
|
49273
49318
|
},
|
|
49274
49319
|
lastInspectedAt,
|
|
49275
49320
|
lastSyncedAt,
|
|
@@ -49887,7 +49932,7 @@ async function googleRoutes(app, opts) {
|
|
|
49887
49932
|
valueThreshold: r.valueThreshold ?? null
|
|
49888
49933
|
})),
|
|
49889
49934
|
total: rows.length,
|
|
49890
|
-
thresholdedPct:
|
|
49935
|
+
thresholdedPct: percentOf(thresholded, rows.length) ?? 0
|
|
49891
49936
|
};
|
|
49892
49937
|
});
|
|
49893
49938
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
@@ -57212,7 +57257,7 @@ async function bingRoutes(app, opts) {
|
|
|
57212
57257
|
indexed,
|
|
57213
57258
|
notIndexed,
|
|
57214
57259
|
unknown,
|
|
57215
|
-
percentage:
|
|
57260
|
+
percentage: percentOf(indexed, total) ?? 0
|
|
57216
57261
|
},
|
|
57217
57262
|
lastInspectedAt,
|
|
57218
57263
|
indexed: indexedUrls.map(formatRow),
|
|
@@ -57658,6 +57703,47 @@ function buildSessionHistory(landingPageDays, dailyTotals) {
|
|
|
57658
57703
|
});
|
|
57659
57704
|
}
|
|
57660
57705
|
|
|
57706
|
+
// ../api-routes/src/ga-source-mover.ts
|
|
57707
|
+
function moverChangeBasis(prior) {
|
|
57708
|
+
if (prior <= 0) return GaMoverChangeBases.new;
|
|
57709
|
+
return prior < MIN_PCT_BASE ? GaMoverChangeBases["small-base"] : GaMoverChangeBases.percent;
|
|
57710
|
+
}
|
|
57711
|
+
function buildSourceMover(source, sessions7d, sessionsPrev7d) {
|
|
57712
|
+
return {
|
|
57713
|
+
source,
|
|
57714
|
+
sessions7d,
|
|
57715
|
+
sessionsPrev7d,
|
|
57716
|
+
changeSessions: sessions7d - sessionsPrev7d,
|
|
57717
|
+
// Null from a zero prior. It used to fall back to 100, so every source that
|
|
57718
|
+
// appeared this week read as "+100%" whatever it sent.
|
|
57719
|
+
changePct: deltaPercent(sessions7d, sessionsPrev7d),
|
|
57720
|
+
changeBasis: moverChangeBasis(sessionsPrev7d)
|
|
57721
|
+
};
|
|
57722
|
+
}
|
|
57723
|
+
function findBiggestMover(current, prior) {
|
|
57724
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
57725
|
+
const entry = (source) => {
|
|
57726
|
+
let sessions = bySource.get(source);
|
|
57727
|
+
if (!sessions) {
|
|
57728
|
+
sessions = { current: 0, prior: 0 };
|
|
57729
|
+
bySource.set(source, sessions);
|
|
57730
|
+
}
|
|
57731
|
+
return sessions;
|
|
57732
|
+
};
|
|
57733
|
+
for (const row of current) entry(row.source).current += row.sessions ?? 0;
|
|
57734
|
+
for (const row of prior) entry(row.source).prior += row.sessions ?? 0;
|
|
57735
|
+
let mover = null;
|
|
57736
|
+
const sources = [...bySource.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
57737
|
+
for (const source of sources) {
|
|
57738
|
+
const sessions = bySource.get(source);
|
|
57739
|
+
const size = Math.abs(sessions.current - sessions.prior);
|
|
57740
|
+
if (size > 0 && (mover === null || size > Math.abs(mover.changeSessions))) {
|
|
57741
|
+
mover = buildSourceMover(source, sessions.current, sessions.prior);
|
|
57742
|
+
}
|
|
57743
|
+
}
|
|
57744
|
+
return mover;
|
|
57745
|
+
}
|
|
57746
|
+
|
|
57661
57747
|
// ../api-routes/src/ga.ts
|
|
57662
57748
|
function gaLog(level, action, ctx) {
|
|
57663
57749
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
@@ -57665,12 +57751,11 @@ function gaLog(level, action, ctx) {
|
|
|
57665
57751
|
stream.write(JSON.stringify(entry) + "\n");
|
|
57666
57752
|
}
|
|
57667
57753
|
function formatSharePct(numerator, total) {
|
|
57668
|
-
|
|
57669
|
-
|
|
57670
|
-
|
|
57671
|
-
const
|
|
57672
|
-
|
|
57673
|
-
return `${rounded}%`;
|
|
57754
|
+
return formatPercent(shareOf(numerator, total));
|
|
57755
|
+
}
|
|
57756
|
+
function withSessionShares(rows) {
|
|
57757
|
+
const shares = breakdownShares(rows.map((row) => row.sessions));
|
|
57758
|
+
return rows.map((row, index2) => ({ ...row, share: shares[index2] ?? 0 }));
|
|
57674
57759
|
}
|
|
57675
57760
|
function dateRangeConditions(column, range) {
|
|
57676
57761
|
const conditions = [];
|
|
@@ -57694,7 +57779,7 @@ function buildChannelBreakdown(input) {
|
|
|
57694
57779
|
const otherSessions = Math.max(0, input.totalSessions - coveredSessions);
|
|
57695
57780
|
const bucket = (sessions) => ({
|
|
57696
57781
|
sessions,
|
|
57697
|
-
sharePct:
|
|
57782
|
+
sharePct: percentOf(sessions, input.totalSessions) ?? 0,
|
|
57698
57783
|
sharePctDisplay: formatSharePct(sessions, input.totalSessions)
|
|
57699
57784
|
});
|
|
57700
57785
|
return {
|
|
@@ -57704,7 +57789,7 @@ function buildChannelBreakdown(input) {
|
|
|
57704
57789
|
ai: bucket(aiSessions),
|
|
57705
57790
|
other: {
|
|
57706
57791
|
sessions: otherSessions,
|
|
57707
|
-
sharePct:
|
|
57792
|
+
sharePct: percentOf(otherSessions, input.totalSessions) ?? 0,
|
|
57708
57793
|
sharePctDisplay: input.totalSessions <= 0 && coveredSessions > 0 ? "\u2014" : formatSharePct(otherSessions, input.totalSessions)
|
|
57709
57794
|
}
|
|
57710
57795
|
};
|
|
@@ -58536,7 +58621,8 @@ async function ga4Routes(app, opts) {
|
|
|
58536
58621
|
sessions: r.sessions ?? 0,
|
|
58537
58622
|
organicSessions: r.organicSessions ?? 0,
|
|
58538
58623
|
directSessions: r.directSessions ?? 0,
|
|
58539
|
-
users: r.users ?? 0
|
|
58624
|
+
users: r.users ?? 0,
|
|
58625
|
+
organicShare: shareOf(r.organicSessions ?? 0, r.sessions ?? 0)
|
|
58540
58626
|
})),
|
|
58541
58627
|
// No `users` on either referral row type, and no `ai*Users*` totals: GA
|
|
58542
58628
|
// reports users as a COUNT DISTINCT at the grain requested, so summing
|
|
@@ -58544,13 +58630,16 @@ async function ga4Routes(app, opts) {
|
|
|
58544
58630
|
// every date in the window) counted one visitor once per row. Withdrawn
|
|
58545
58631
|
// in 4.135.0; see `ga-ai-referral-aggregation.ts` for why no correct
|
|
58546
58632
|
// figure can be computed or fetched.
|
|
58547
|
-
|
|
58633
|
+
// Shares of the rows' own sum, not of `aiSessionsDeduped`: that total
|
|
58634
|
+
// keeps the winning lens per day, these rows one lens per source over the
|
|
58635
|
+
// window, so it can exceed what the rows add up to.
|
|
58636
|
+
aiReferrals: withSessionShares(aiReferrals.map((r) => ({
|
|
58548
58637
|
source: r.source,
|
|
58549
58638
|
medium: r.medium,
|
|
58550
58639
|
trafficClass: normalizeAiTrafficClass(r.trafficClass),
|
|
58551
58640
|
sourceDimension: r.sourceDimension,
|
|
58552
58641
|
sessions: r.sessions ?? 0
|
|
58553
|
-
})),
|
|
58642
|
+
}))),
|
|
58554
58643
|
aiReferralLandingPages: aiReferralLandingPages.map((r) => ({
|
|
58555
58644
|
source: r.source,
|
|
58556
58645
|
medium: r.medium,
|
|
@@ -58565,23 +58654,25 @@ async function ga4Routes(app, opts) {
|
|
|
58565
58654
|
aiSessionsBySession: aiSummary.bySession.sessions,
|
|
58566
58655
|
paidAiSessionsBySession: aiSummary.paidBySession.sessions,
|
|
58567
58656
|
organicAiSessionsBySession: aiSummary.organicBySession.sessions,
|
|
58568
|
-
|
|
58657
|
+
// These rows partition `socialSessions` (same window, one lens), so each
|
|
58658
|
+
// share is also the row's share of that total.
|
|
58659
|
+
socialReferrals: withSessionShares(socialReferrals.map((r) => ({
|
|
58569
58660
|
source: r.source,
|
|
58570
58661
|
medium: r.medium,
|
|
58571
58662
|
channelGroup: r.channelGroup,
|
|
58572
58663
|
sessions: r.sessions ?? 0
|
|
58573
|
-
})),
|
|
58664
|
+
}))),
|
|
58574
58665
|
socialSessions,
|
|
58575
58666
|
channelBreakdown,
|
|
58576
|
-
organicSharePct:
|
|
58577
|
-
aiSharePct:
|
|
58578
|
-
aiSharePctBySession:
|
|
58579
|
-
paidAiSharePct:
|
|
58580
|
-
paidAiSharePctBySession:
|
|
58581
|
-
organicAiSharePct:
|
|
58582
|
-
organicAiSharePctBySession:
|
|
58583
|
-
directSharePct:
|
|
58584
|
-
socialSharePct:
|
|
58667
|
+
organicSharePct: percentOf(totalOrganicSessions, total) ?? 0,
|
|
58668
|
+
aiSharePct: percentOf(aiSummary.deduped.sessions, total) ?? 0,
|
|
58669
|
+
aiSharePctBySession: percentOf(aiSummary.bySession.sessions, total) ?? 0,
|
|
58670
|
+
paidAiSharePct: percentOf(aiSummary.paidDeduped.sessions, total) ?? 0,
|
|
58671
|
+
paidAiSharePctBySession: percentOf(aiSummary.paidBySession.sessions, total) ?? 0,
|
|
58672
|
+
organicAiSharePct: percentOf(aiSummary.organicDeduped.sessions, total) ?? 0,
|
|
58673
|
+
organicAiSharePctBySession: percentOf(aiSummary.organicBySession.sessions, total) ?? 0,
|
|
58674
|
+
directSharePct: percentOf(totalDirectSessions, total) ?? 0,
|
|
58675
|
+
socialSharePct: percentOf(socialSessions, total) ?? 0,
|
|
58585
58676
|
otherSessions: channelBreakdown.other.sessions,
|
|
58586
58677
|
otherSharePct: channelBreakdown.other.sharePct,
|
|
58587
58678
|
otherSharePctDisplay: channelBreakdown.other.sharePctDisplay,
|
|
@@ -58703,31 +58794,16 @@ async function ga4Routes(app, opts) {
|
|
|
58703
58794
|
sql21`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
58704
58795
|
sql21`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
58705
58796
|
)).groupBy(gaSocialReferrals.source).all();
|
|
58706
|
-
const
|
|
58707
|
-
let biggestMover = null;
|
|
58708
|
-
let maxDelta = 0;
|
|
58709
|
-
for (const row of sourceCurrent) {
|
|
58710
|
-
const prev = prevMap.get(row.source) ?? 0;
|
|
58711
|
-
const delta = Math.abs(row.sessions - prev);
|
|
58712
|
-
if (delta > maxDelta) {
|
|
58713
|
-
maxDelta = delta;
|
|
58714
|
-
biggestMover = {
|
|
58715
|
-
source: row.source,
|
|
58716
|
-
sessions7d: row.sessions,
|
|
58717
|
-
sessionsPrev7d: prev,
|
|
58718
|
-
changePct: deltaPercent(row.sessions, prev) ?? (row.sessions > 0 ? 100 : 0)
|
|
58719
|
-
};
|
|
58720
|
-
}
|
|
58721
|
-
}
|
|
58722
|
-
return {
|
|
58797
|
+
const response = {
|
|
58723
58798
|
socialSessions7d: current7d?.sessions ?? 0,
|
|
58724
58799
|
socialSessionsPrev7d: prev7d?.sessions ?? 0,
|
|
58725
58800
|
trend7dPct: deltaPercent(current7d?.sessions ?? 0, prev7d?.sessions ?? 0),
|
|
58726
58801
|
socialSessions30d: current30d?.sessions ?? 0,
|
|
58727
58802
|
socialSessionsPrev30d: prev30d?.sessions ?? 0,
|
|
58728
58803
|
trend30dPct: deltaPercent(current30d?.sessions ?? 0, prev30d?.sessions ?? 0),
|
|
58729
|
-
biggestMover
|
|
58804
|
+
biggestMover: findBiggestMover(sourceCurrent, sourcePrev)
|
|
58730
58805
|
};
|
|
58806
|
+
return response;
|
|
58731
58807
|
});
|
|
58732
58808
|
app.get("/projects/:name/ga/attribution-trend", async (request, _reply) => {
|
|
58733
58809
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -58769,23 +58845,9 @@ async function ga4Routes(app, opts) {
|
|
|
58769
58845
|
sql21`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
58770
58846
|
eq55(gaAiReferrals.sourceDimension, "session")
|
|
58771
58847
|
)).groupBy(gaAiReferrals.source).all();
|
|
58772
|
-
const findBiggestMover = (current, prev) => {
|
|
58773
|
-
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
58774
|
-
let mover = null;
|
|
58775
|
-
let maxDelta = 0;
|
|
58776
|
-
for (const row of current) {
|
|
58777
|
-
const p = prevMap.get(row.source) ?? 0;
|
|
58778
|
-
const delta = Math.abs(row.sessions - p);
|
|
58779
|
-
if (delta > maxDelta) {
|
|
58780
|
-
maxDelta = delta;
|
|
58781
|
-
mover = { source: row.source, sessions7d: row.sessions, sessionsPrev7d: p, changePct: deltaPercent(row.sessions, p) ?? (row.sessions > 0 ? 100 : 0) };
|
|
58782
|
-
}
|
|
58783
|
-
}
|
|
58784
|
-
return mover;
|
|
58785
|
-
};
|
|
58786
58848
|
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql21`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and47(eq55(gaSocialReferrals.projectId, project.id), sql21`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql21`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
58787
58849
|
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql21`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and47(eq55(gaSocialReferrals.projectId, project.id), sql21`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql21`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
58788
|
-
|
|
58850
|
+
const response = {
|
|
58789
58851
|
total: buildTrend(sumTotal),
|
|
58790
58852
|
organic: buildTrend(sumOrganic),
|
|
58791
58853
|
ai: buildTrend(sumAi),
|
|
@@ -58794,6 +58856,7 @@ async function ga4Routes(app, opts) {
|
|
|
58794
58856
|
aiBiggestMover: findBiggestMover(aiSourceCurrent, aiSourcePrev),
|
|
58795
58857
|
socialBiggestMover: findBiggestMover(socialSourceCurrent, socialSourcePrev)
|
|
58796
58858
|
};
|
|
58859
|
+
return response;
|
|
58797
58860
|
});
|
|
58798
58861
|
app.get("/projects/:name/ga/session-history", async (request, _reply) => {
|
|
58799
58862
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -82342,9 +82405,6 @@ function loadProject(ctx) {
|
|
|
82342
82405
|
if (!ctx.project) return null;
|
|
82343
82406
|
return ctx.db.select().from(projects).where(eq66(projects.id, ctx.project.id)).get() ?? null;
|
|
82344
82407
|
}
|
|
82345
|
-
function percent(value) {
|
|
82346
|
-
return Math.round(value * 100);
|
|
82347
|
-
}
|
|
82348
82408
|
var winnabilityCoverageCheck = {
|
|
82349
82409
|
id: "content.winnability.coverage",
|
|
82350
82410
|
category: CheckCategories.integrations,
|
|
@@ -82412,7 +82472,7 @@ var winnabilityCoverageCheck = {
|
|
|
82412
82472
|
return {
|
|
82413
82473
|
status: CheckStatuses.warn,
|
|
82414
82474
|
code: "content.winnability.low-coverage",
|
|
82415
|
-
summary: `${coveredDomains.length} of ${citedDomains.length} cited-surface domain(s) recognized (${
|
|
82475
|
+
summary: `${coveredDomains.length} of ${citedDomains.length} cited-surface domain(s) recognized (${formatPercent(coverage)}); the winnability gate may miss ceded surfaces in the unrecognized tail.`,
|
|
82416
82476
|
remediation: discoverRemediation,
|
|
82417
82477
|
details
|
|
82418
82478
|
};
|
|
@@ -82420,7 +82480,7 @@ var winnabilityCoverageCheck = {
|
|
|
82420
82480
|
return {
|
|
82421
82481
|
status: CheckStatuses.ok,
|
|
82422
82482
|
code: "content.winnability.covered",
|
|
82423
|
-
summary: `${coveredDomains.length} of ${citedDomains.length} cited-surface domain(s) recognized (${
|
|
82483
|
+
summary: `${coveredDomains.length} of ${citedDomains.length} cited-surface domain(s) recognized (${formatPercent(coverage)}); the winnability gate is active.`,
|
|
82424
82484
|
remediation: null,
|
|
82425
82485
|
details
|
|
82426
82486
|
};
|
|
@@ -86113,6 +86173,9 @@ function mapCrawlPage(row) {
|
|
|
86113
86173
|
healthState: deriveSiteHealthState(row)
|
|
86114
86174
|
};
|
|
86115
86175
|
}
|
|
86176
|
+
function withRecordedShare(factor) {
|
|
86177
|
+
return { ...factor, sharePct: typeof factor.sharePct === "number" ? factor.sharePct : null };
|
|
86178
|
+
}
|
|
86116
86179
|
function mapCrawlPageAuditEvidence(row) {
|
|
86117
86180
|
const fields = row.auditFields;
|
|
86118
86181
|
const rawFactors = Array.isArray(fields.factors) ? fields.factors : [];
|
|
@@ -86399,7 +86462,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
86399
86462
|
trend,
|
|
86400
86463
|
previousScore: previous?.aggregateScore ?? null,
|
|
86401
86464
|
previousAuditedAt: previous?.auditedAt ?? null,
|
|
86402
|
-
factors: snap.factorAverages,
|
|
86465
|
+
factors: snap.factorAverages.map(withRecordedShare),
|
|
86403
86466
|
crossCuttingIssues: snap.crossCuttingIssues,
|
|
86404
86467
|
prioritizedFixes: snap.prioritizedFixes
|
|
86405
86468
|
};
|
|
@@ -86436,7 +86499,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
86436
86499
|
overallScore: row.overallScore,
|
|
86437
86500
|
status: row.status === "error" ? "error" : "success",
|
|
86438
86501
|
error: row.error,
|
|
86439
|
-
factors: row.factors
|
|
86502
|
+
factors: row.factors.map(withRecordedShare)
|
|
86440
86503
|
}));
|
|
86441
86504
|
return { project: project.name, runId: latest.runId, auditedAt: latest.auditedAt, total, pages };
|
|
86442
86505
|
});
|
|
@@ -90164,6 +90227,7 @@ export {
|
|
|
90164
90227
|
deliverWebhook,
|
|
90165
90228
|
redactNotificationUrl,
|
|
90166
90229
|
renderReportHtml,
|
|
90230
|
+
buildOpenApiDocument,
|
|
90167
90231
|
toAlertView,
|
|
90168
90232
|
resolveDestination,
|
|
90169
90233
|
getPlaceDetails,
|