@canonry/canonry 4.133.3 → 4.134.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/README.md +0 -13
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +18 -0
- package/assets/assets/{AuditHistoryPanel-DTHQGvkW.js → AuditHistoryPanel-DBUWbfNk.js} +1 -1
- package/assets/assets/{BacklinksPage-DuPI3wt9.js → BacklinksPage-BFm0VdUO.js} +1 -1
- package/assets/assets/{ChartPrimitives-T8mxOObc.js → ChartPrimitives-oV_nduTH.js} +1 -1
- package/assets/assets/{HistoryPage-T1W40xKG.js → HistoryPage-IF2FMem3.js} +1 -1
- package/assets/assets/{ProjectPage-DYwM8-Rp.js → ProjectPage-ClwxX9Ga.js} +2 -2
- package/assets/assets/{RunRow-nHr4wW9B.js → RunRow-De5zdch5.js} +1 -1
- package/assets/assets/{RunsPage-Cbusy_84.js → RunsPage-xtRF78r8.js} +1 -1
- package/assets/assets/{SettingsPage-BVcbl4gn.js → SettingsPage-B_GAxaWu.js} +1 -1
- package/assets/assets/{TrafficPage-CZ4NTWI6.js → TrafficPage-E8I5ineJ.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-xW-KvZY1.js → TrafficSourceDetailPage-DGk2MqSp.js} +1 -1
- package/assets/assets/{arrow-left-flskyFPK.js → arrow-left-D-4QMxi9.js} +1 -1
- package/assets/assets/{extract-error-message-BHfCTm0H.js → extract-error-message-DuqIFU8Z.js} +1 -1
- package/assets/assets/{index-CQzGDGMB.js → index-De1MKbc7.js} +88 -88
- package/assets/assets/{trash-2-LGfqtSzZ.js → trash-2-BVh__r6G.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-HENMGNLT.js → chunk-LIXWDIDR.js} +496 -3
- package/dist/{chunk-YRRQFOAH.js → chunk-PC7AYXND.js} +233 -2
- package/dist/{chunk-6ON6GUAL.js → chunk-TKXWTDV6.js} +68 -6
- package/dist/{chunk-A6BYMSZL.js → chunk-ZUQPHGEN.js} +45 -2
- package/dist/cli.js +68 -4
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-OSLFG5S2.js → intelligence-service-4XOFUN5E.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as a}from"./index-
|
|
1
|
+
import{c as a}from"./index-De1MKbc7.js";const c=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],d=a("circle-check",c);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],k=a("circle-question-mark",e);const t=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=a("download",t);const o=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],r=a("external-link",o);const n=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],y=a("trash-2",n);export{d as C,s as D,r as E,y as T,k as a};
|
package/assets/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-De1MKbc7.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/vendor-recharts-DSxTZhaH.js">
|
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
AdsHistoricalCampaignRollupStatuses,
|
|
20
20
|
AdsInsightLevels,
|
|
21
21
|
AdsIntegrityDecisions,
|
|
22
|
+
AdsLiveDeliveryBases,
|
|
23
|
+
AdsLiveEntityTypes,
|
|
24
|
+
AdsLivePresences,
|
|
22
25
|
AdsOperationKinds,
|
|
23
26
|
AdsOperationStates,
|
|
24
27
|
AdsOperationStepStates,
|
|
@@ -96,6 +99,8 @@ import {
|
|
|
96
99
|
adsImageUploadRequestSchema,
|
|
97
100
|
adsInsightLevelSchema,
|
|
98
101
|
adsInsightsResponseSchema,
|
|
102
|
+
adsLiveDeliveryDtoSchema,
|
|
103
|
+
adsLiveDeliveryQuerySchema,
|
|
99
104
|
adsOperationReconcileResponseSchema,
|
|
100
105
|
adsOperationResponseSchema,
|
|
101
106
|
adsOperationStepDtoSchema,
|
|
@@ -183,6 +188,7 @@ import {
|
|
|
183
188
|
discoverySessionDetailDtoSchema,
|
|
184
189
|
discoverySessionDtoSchema,
|
|
185
190
|
doctorReportSchema,
|
|
191
|
+
dollarsToMicros,
|
|
186
192
|
domainClassificationsResponseDtoSchema,
|
|
187
193
|
effectiveBrandNames,
|
|
188
194
|
effectiveDomains,
|
|
@@ -199,6 +205,7 @@ import {
|
|
|
199
205
|
formatDateRange,
|
|
200
206
|
formatDeltaCopy,
|
|
201
207
|
formatIsoDate,
|
|
208
|
+
formatIsoDateInTimeZone,
|
|
202
209
|
formatNumber,
|
|
203
210
|
formatRatio,
|
|
204
211
|
formatWindowCountDelta,
|
|
@@ -242,6 +249,7 @@ import {
|
|
|
242
249
|
internalError,
|
|
243
250
|
isBrowserProvider,
|
|
244
251
|
isReadOnlyKey,
|
|
252
|
+
isoDateDaysBeforeInTimeZone,
|
|
245
253
|
keywordDtoSchema,
|
|
246
254
|
keywordGenerateRequestSchema,
|
|
247
255
|
latestProjectRunDtoSchema,
|
|
@@ -356,7 +364,7 @@ import {
|
|
|
356
364
|
wordpressSchemaDeployResultDtoSchema,
|
|
357
365
|
wordpressSchemaStatusResultDtoSchema,
|
|
358
366
|
wordpressStatusDtoSchema
|
|
359
|
-
} from "./chunk-
|
|
367
|
+
} from "./chunk-PC7AYXND.js";
|
|
360
368
|
|
|
361
369
|
// src/intelligence-service.ts
|
|
362
370
|
import { eq as eq44, desc as desc21, asc as asc9, and as and35, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
|
|
@@ -1462,6 +1470,8 @@ var researchRunQueries = sqliteTable("research_run_queries", {
|
|
|
1462
1470
|
groundingSources: text("grounding_sources", { mode: "json" }).$type().notNull().default([]),
|
|
1463
1471
|
citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
|
|
1464
1472
|
searchQueries: text("search_queries", { mode: "json" }).$type().notNull().default([]),
|
|
1473
|
+
namedCompetitors: text("named_competitors", { mode: "json" }).$type().notNull().default([]),
|
|
1474
|
+
citedCompetitorDomains: text("cited_competitor_domains", { mode: "json" }).$type().notNull().default([]),
|
|
1465
1475
|
answerMentioned: integer("answer_mentioned", { mode: "boolean" }),
|
|
1466
1476
|
citationState: text("citation_state"),
|
|
1467
1477
|
rawResponse: text("raw_response", { mode: "json" }).$type(),
|
|
@@ -4266,6 +4276,16 @@ var MIGRATION_VERSIONS = [
|
|
|
4266
4276
|
updated_at TEXT NOT NULL
|
|
4267
4277
|
)`
|
|
4268
4278
|
]
|
|
4279
|
+
},
|
|
4280
|
+
{
|
|
4281
|
+
// Keep research batches useful as a competitor-comparison workspace while
|
|
4282
|
+
// retaining their strict isolation from tracked visibility snapshots.
|
|
4283
|
+
version: 110,
|
|
4284
|
+
name: "research-query-named-and-cited-competitors",
|
|
4285
|
+
statements: [
|
|
4286
|
+
`ALTER TABLE research_run_queries ADD COLUMN named_competitors TEXT NOT NULL DEFAULT '[]'`,
|
|
4287
|
+
`ALTER TABLE research_run_queries ADD COLUMN cited_competitor_domains TEXT NOT NULL DEFAULT '[]'`
|
|
4288
|
+
]
|
|
4269
4289
|
}
|
|
4270
4290
|
];
|
|
4271
4291
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -17392,6 +17412,7 @@ var SCHEMA_TABLE = {
|
|
|
17392
17412
|
AdsDisconnectResponse: adsDisconnectResponseSchema,
|
|
17393
17413
|
AdsGeoSearchResponse: adsGeoSearchResponseSchema,
|
|
17394
17414
|
AdsInsightsResponse: adsInsightsResponseSchema,
|
|
17415
|
+
AdsLiveDeliveryDto: adsLiveDeliveryDtoSchema,
|
|
17395
17416
|
AdsOperationReconcileResponse: adsOperationReconcileResponseSchema,
|
|
17396
17417
|
AdsOperationResponse: adsOperationResponseSchema,
|
|
17397
17418
|
AdsSummaryDto: adsSummaryDtoSchema,
|
|
@@ -20395,6 +20416,35 @@ var routeCatalog = [
|
|
|
20395
20416
|
404: errorResponse("Project not found.")
|
|
20396
20417
|
}
|
|
20397
20418
|
},
|
|
20419
|
+
{
|
|
20420
|
+
method: "get",
|
|
20421
|
+
path: "/api/v1/projects/{name}/ads/live-delivery",
|
|
20422
|
+
summary: "Live provider read of ads status and metrics, with the local snapshot delta",
|
|
20423
|
+
description: "Calls the OpenAI Ads API right now with the stored connection and returns the provider's current status and metrics unaggregated, alongside the corresponding local snapshot values and an explicit per-entity delta. Dates on both sides are the ad account's local calendar dates. The first day of the window is measured from 00:00 in the account timezone on both sides, so it is a whole day either way and a difference on it is real; the current local day is in progress on both sides, so a difference there is snapshot staleness. Read-only: it never creates, updates, pauses, or activates anything, and it never waits for a sync run. The window counts calendar days in the account timezone, not fixed 24-hour periods, so a daylight-saving transition cannot move the first day. The walk is bounded by per-level caps and a budget of 40 reader calls (see bounds). Admission has two rules: a project may have only one live read running at a time (a second request is refused for as long as the first is actually walking, which can exceed any interval), and consecutive reads are spaced by at least one minute. The interval is counted as soon as the provider is called, so a read that fails upstream still holds it; only a read rejected before the provider was called leaves the next one free. A reader call is not one HTTP request: each list or insight read auto-paginates up to 100 pages, so the worst-case upstream cost of a single request is about 4000 HTTP requests, reported as bounds.maxUpstreamHttpRequests.",
|
|
20424
|
+
tags: ["ads"],
|
|
20425
|
+
parameters: [
|
|
20426
|
+
nameParameter,
|
|
20427
|
+
{
|
|
20428
|
+
name: "campaignId",
|
|
20429
|
+
in: "query",
|
|
20430
|
+
description: "Scope the walk to a single campaign.",
|
|
20431
|
+
schema: stringSchema
|
|
20432
|
+
},
|
|
20433
|
+
{
|
|
20434
|
+
name: "lookbackDays",
|
|
20435
|
+
in: "query",
|
|
20436
|
+
description: "Metrics window in calendar days of the ad account timezone (1-30, default 7).",
|
|
20437
|
+
schema: integerSchema
|
|
20438
|
+
}
|
|
20439
|
+
],
|
|
20440
|
+
responses: {
|
|
20441
|
+
200: jsonResponse("Live provider state and snapshot delta returned.", "AdsLiveDeliveryDto"),
|
|
20442
|
+
400: errorResponse("Invalid query, or no ads connection for this project."),
|
|
20443
|
+
404: errorResponse("Project not found."),
|
|
20444
|
+
429: errorResponse("Another live read for this project is still running, or the minimum interval since the last provider read has not elapsed. error.details.reason says which."),
|
|
20445
|
+
502: errorResponse("The OpenAI Ads API read failed.")
|
|
20446
|
+
}
|
|
20447
|
+
},
|
|
20398
20448
|
{
|
|
20399
20449
|
method: "post",
|
|
20400
20450
|
path: "/api/v1/projects/{name}/bing/connect",
|
|
@@ -28465,6 +28515,116 @@ function registerAdsActivationRoutes(app, opts) {
|
|
|
28465
28515
|
});
|
|
28466
28516
|
}
|
|
28467
28517
|
|
|
28518
|
+
// ../api-routes/src/ads-live-delivery.ts
|
|
28519
|
+
function liveComparisonWindow(fetchedAtMs, lookbackDays, timezone) {
|
|
28520
|
+
const fetchedAtIso = new Date(fetchedAtMs).toISOString();
|
|
28521
|
+
return {
|
|
28522
|
+
startDate: isoDateDaysBeforeInTimeZone(fetchedAtIso, lookbackDays, timezone),
|
|
28523
|
+
endDate: formatIsoDateInTimeZone(fetchedAtIso, timezone)
|
|
28524
|
+
};
|
|
28525
|
+
}
|
|
28526
|
+
var EMPTY_METRIC_VALUES = {
|
|
28527
|
+
impressions: 0,
|
|
28528
|
+
clicks: 0,
|
|
28529
|
+
spendMicros: 0,
|
|
28530
|
+
conversions: 0
|
|
28531
|
+
};
|
|
28532
|
+
function presenceOf(live, stored) {
|
|
28533
|
+
if (live && stored) return AdsLivePresences.both;
|
|
28534
|
+
if (live) return AdsLivePresences["live-only"];
|
|
28535
|
+
return AdsLivePresences["stored-only"];
|
|
28536
|
+
}
|
|
28537
|
+
function fieldDelta(field, live, stored) {
|
|
28538
|
+
return live === stored ? null : { field, live, stored };
|
|
28539
|
+
}
|
|
28540
|
+
function buildFieldDeltas(live, stored) {
|
|
28541
|
+
if (!live || !stored) return [];
|
|
28542
|
+
const deltas = [
|
|
28543
|
+
fieldDelta("status", live.status, stored.status),
|
|
28544
|
+
fieldDelta("reviewStatus", live.reviewStatus, stored.reviewStatus),
|
|
28545
|
+
fieldDelta("name", live.name, stored.name)
|
|
28546
|
+
];
|
|
28547
|
+
return deltas.filter((delta) => delta !== null);
|
|
28548
|
+
}
|
|
28549
|
+
function foldLiveRows(rows) {
|
|
28550
|
+
let values = EMPTY_METRIC_VALUES;
|
|
28551
|
+
for (const row of rows) {
|
|
28552
|
+
values = {
|
|
28553
|
+
impressions: values.impressions + Math.round(row.impressions ?? 0),
|
|
28554
|
+
clicks: values.clicks + Math.round(row.clicks ?? 0),
|
|
28555
|
+
spendMicros: values.spendMicros + dollarsToMicros(row.spend ?? 0),
|
|
28556
|
+
conversions: values.conversions + Math.round(row.conversions ?? 0)
|
|
28557
|
+
};
|
|
28558
|
+
}
|
|
28559
|
+
return values;
|
|
28560
|
+
}
|
|
28561
|
+
function sameMetrics(a, b) {
|
|
28562
|
+
return a.impressions === b.impressions && a.clicks === b.clicks && a.spendMicros === b.spendMicros && a.conversions === b.conversions;
|
|
28563
|
+
}
|
|
28564
|
+
function buildMetricDeltas(liveRows, storedRows, window) {
|
|
28565
|
+
if (liveRows === null && storedRows === null) return null;
|
|
28566
|
+
const liveByDate = /* @__PURE__ */ new Map();
|
|
28567
|
+
for (const row of liveRows ?? []) {
|
|
28568
|
+
if (row.date === null) continue;
|
|
28569
|
+
const bucket = liveByDate.get(row.date) ?? [];
|
|
28570
|
+
bucket.push(row);
|
|
28571
|
+
liveByDate.set(row.date, bucket);
|
|
28572
|
+
}
|
|
28573
|
+
const storedByDate = /* @__PURE__ */ new Map();
|
|
28574
|
+
for (const row of storedRows ?? []) {
|
|
28575
|
+
if (row.date < window.startDate || row.date > window.endDate) continue;
|
|
28576
|
+
storedByDate.set(row.date, row);
|
|
28577
|
+
}
|
|
28578
|
+
const dates = [.../* @__PURE__ */ new Set([...liveByDate.keys(), ...storedByDate.keys()])].sort();
|
|
28579
|
+
return dates.map((date) => {
|
|
28580
|
+
const liveBucket = liveByDate.get(date);
|
|
28581
|
+
const storedRow = storedByDate.get(date);
|
|
28582
|
+
const live = liveBucket ? foldLiveRows(liveBucket) : null;
|
|
28583
|
+
const stored = storedRow ? {
|
|
28584
|
+
impressions: storedRow.impressions,
|
|
28585
|
+
clicks: storedRow.clicks,
|
|
28586
|
+
spendMicros: storedRow.spendMicros,
|
|
28587
|
+
conversions: storedRow.conversions
|
|
28588
|
+
} : null;
|
|
28589
|
+
const drifted = live === null || stored === null || !sameMetrics(live, stored);
|
|
28590
|
+
return { date, live, stored, drifted };
|
|
28591
|
+
});
|
|
28592
|
+
}
|
|
28593
|
+
function buildLiveEntityComparison(observation, window) {
|
|
28594
|
+
const presence = presenceOf(observation.live, observation.stored);
|
|
28595
|
+
const fieldDeltas = buildFieldDeltas(observation.live, observation.stored);
|
|
28596
|
+
const metricDeltas = buildMetricDeltas(observation.liveMetrics, observation.storedMetrics, window);
|
|
28597
|
+
const drifted = presence !== AdsLivePresences.both || fieldDeltas.length > 0 || (metricDeltas?.some((delta) => delta.drifted) ?? false);
|
|
28598
|
+
return {
|
|
28599
|
+
entityType: observation.entityType,
|
|
28600
|
+
id: observation.id,
|
|
28601
|
+
parentId: observation.parentId,
|
|
28602
|
+
presence,
|
|
28603
|
+
live: observation.live,
|
|
28604
|
+
stored: observation.stored,
|
|
28605
|
+
fieldDeltas,
|
|
28606
|
+
liveMetrics: observation.liveMetrics,
|
|
28607
|
+
metricDeltas,
|
|
28608
|
+
drifted
|
|
28609
|
+
};
|
|
28610
|
+
}
|
|
28611
|
+
function summarizeLiveDrift(entities) {
|
|
28612
|
+
let driftedEntities = 0;
|
|
28613
|
+
let statusDrifted = 0;
|
|
28614
|
+
let metricsDrifted = 0;
|
|
28615
|
+
for (const entity of entities) {
|
|
28616
|
+
if (entity.drifted) driftedEntities += 1;
|
|
28617
|
+
if (entity.fieldDeltas.some((delta) => delta.field === "status")) statusDrifted += 1;
|
|
28618
|
+
if (entity.metricDeltas?.some((delta) => delta.drifted)) metricsDrifted += 1;
|
|
28619
|
+
}
|
|
28620
|
+
return {
|
|
28621
|
+
entitiesCompared: entities.length,
|
|
28622
|
+
driftedEntities,
|
|
28623
|
+
statusDrifted,
|
|
28624
|
+
metricsDrifted
|
|
28625
|
+
};
|
|
28626
|
+
}
|
|
28627
|
+
|
|
28468
28628
|
// ../api-routes/src/ads.ts
|
|
28469
28629
|
function runSourceDto(row) {
|
|
28470
28630
|
const parsed = runStatusSchema.safeParse(row.status);
|
|
@@ -28599,6 +28759,12 @@ function storedActivityAssessment(snapshotStatus, rollupStatus, rollupImpression
|
|
|
28599
28759
|
}
|
|
28600
28760
|
}
|
|
28601
28761
|
}
|
|
28762
|
+
var ADS_LIVE_MAX_CAMPAIGNS = 5;
|
|
28763
|
+
var ADS_LIVE_MAX_AD_GROUPS_PER_CAMPAIGN = 10;
|
|
28764
|
+
var ADS_LIVE_MAX_ADS_PER_AD_GROUP = 20;
|
|
28765
|
+
var ADS_LIVE_MAX_READER_CALLS = 40;
|
|
28766
|
+
var DEFAULT_ADS_LIVE_MAX_PAGES_PER_READER_CALL = 100;
|
|
28767
|
+
var DEFAULT_ADS_LIVE_DELIVERY_MIN_INTERVAL_MS = 6e4;
|
|
28602
28768
|
var DEFAULT_RECONCILE_SWEEP_INTERVAL_MS = 6e4;
|
|
28603
28769
|
var DEFAULT_RECONCILE_PENDING_STALE_MS = 5 * 6e4;
|
|
28604
28770
|
var DEFAULT_RECONCILE_BACKOFF_BASE_MS = 5 * 6e4;
|
|
@@ -28961,6 +29127,47 @@ async function executeAdsRead(surface, read) {
|
|
|
28961
29127
|
});
|
|
28962
29128
|
}
|
|
28963
29129
|
}
|
|
29130
|
+
function resolveAdsLiveDeliveryReader(opts, projectName) {
|
|
29131
|
+
const apiKey = opts.adsCredentialStore?.getConnection(projectName)?.apiKey;
|
|
29132
|
+
if (!apiKey) {
|
|
29133
|
+
throw validationError("No OpenAI Ads API key configured for this project");
|
|
29134
|
+
}
|
|
29135
|
+
if (!opts.adsLiveDeliveryReader) {
|
|
29136
|
+
throw validationError("Ads live delivery reads are not configured for this deployment");
|
|
29137
|
+
}
|
|
29138
|
+
return { apiKey, reader: opts.adsLiveDeliveryReader };
|
|
29139
|
+
}
|
|
29140
|
+
function upstreamStatusOf(error) {
|
|
29141
|
+
return typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && Number.isInteger(error.status) ? error.status : null;
|
|
29142
|
+
}
|
|
29143
|
+
function liveReadFailure(surface, entityId, error) {
|
|
29144
|
+
return { surface, entityId, upstreamStatus: upstreamStatusOf(error) };
|
|
29145
|
+
}
|
|
29146
|
+
function liveReadProviderError(surface, error) {
|
|
29147
|
+
const upstreamStatus = upstreamStatusOf(error);
|
|
29148
|
+
return providerError(
|
|
29149
|
+
`OpenAI Ads API ${surface} read failed`,
|
|
29150
|
+
upstreamStatus === null ? void 0 : { upstreamStatus }
|
|
29151
|
+
);
|
|
29152
|
+
}
|
|
29153
|
+
function liveEntityState(entity) {
|
|
29154
|
+
return {
|
|
29155
|
+
name: entity.name,
|
|
29156
|
+
status: entity.status,
|
|
29157
|
+
reviewStatus: entity.reviewStatus,
|
|
29158
|
+
mode: entity.mode,
|
|
29159
|
+
updatedAt: entity.updatedAt
|
|
29160
|
+
};
|
|
29161
|
+
}
|
|
29162
|
+
function storedEntityState(row) {
|
|
29163
|
+
return {
|
|
29164
|
+
name: row.name,
|
|
29165
|
+
status: row.status,
|
|
29166
|
+
reviewStatus: row.reviewStatus ?? null,
|
|
29167
|
+
upstreamUpdatedAt: row.upstreamUpdatedAt,
|
|
29168
|
+
syncedAt: row.syncedAt
|
|
29169
|
+
};
|
|
29170
|
+
}
|
|
28964
29171
|
async function executeAdsOperation(app, request, input) {
|
|
28965
29172
|
const hash = requestHash({ kind: input.kind, entityType: input.entityType, payload: input.payload });
|
|
28966
29173
|
const existing = readOperationByKey(app, input.projectId, input.operationKey);
|
|
@@ -29599,6 +29806,21 @@ async function adsRoutes(app, opts) {
|
|
|
29599
29806
|
opts.adsAccountVerificationCacheTtlMs ?? DEFAULT_ADS_ACCOUNT_VERIFICATION_CACHE_TTL_MS
|
|
29600
29807
|
);
|
|
29601
29808
|
const verificationCache = /* @__PURE__ */ new Map();
|
|
29809
|
+
const liveDeliveryMinIntervalMs = Math.max(
|
|
29810
|
+
0,
|
|
29811
|
+
opts.adsLiveDeliveryMinIntervalMs ?? DEFAULT_ADS_LIVE_DELIVERY_MIN_INTERVAL_MS
|
|
29812
|
+
);
|
|
29813
|
+
const liveDeliveryMaxPagesPerReaderCall = Math.max(
|
|
29814
|
+
1,
|
|
29815
|
+
opts.adsLiveDeliveryMaxPagesPerReaderCall ?? DEFAULT_ADS_LIVE_MAX_PAGES_PER_READER_CALL
|
|
29816
|
+
);
|
|
29817
|
+
const liveDeliveryInFlight = /* @__PURE__ */ new Set();
|
|
29818
|
+
const liveDeliveryLastAttemptAtMs = /* @__PURE__ */ new Map();
|
|
29819
|
+
const pruneLiveDeliveryAttempts = (nowMs) => {
|
|
29820
|
+
for (const [projectId, lastAtMs] of liveDeliveryLastAttemptAtMs) {
|
|
29821
|
+
if (nowMs - lastAtMs >= liveDeliveryMinIntervalMs) liveDeliveryLastAttemptAtMs.delete(projectId);
|
|
29822
|
+
}
|
|
29823
|
+
};
|
|
29602
29824
|
const reconcilePolicy = {
|
|
29603
29825
|
pendingMinIdleMs: pendingStaleMs,
|
|
29604
29826
|
backoffBaseMs,
|
|
@@ -30378,6 +30600,264 @@ async function adsRoutes(app, opts) {
|
|
|
30378
30600
|
};
|
|
30379
30601
|
return response;
|
|
30380
30602
|
});
|
|
30603
|
+
app.get("/projects/:name/ads/live-delivery", async (request) => {
|
|
30604
|
+
const project = resolveProject(app.db, request.params.name);
|
|
30605
|
+
const parsed = adsLiveDeliveryQuerySchema.safeParse(request.query);
|
|
30606
|
+
if (!parsed.success) {
|
|
30607
|
+
throw validationError("Invalid ads live delivery query", { issues: parsed.error.issues });
|
|
30608
|
+
}
|
|
30609
|
+
const { campaignId, lookbackDays } = parsed.data;
|
|
30610
|
+
const connection = app.db.select().from(adsConnections).where(eq28(adsConnections.projectId, project.id)).get();
|
|
30611
|
+
if (!connection) {
|
|
30612
|
+
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
30613
|
+
}
|
|
30614
|
+
const { apiKey, reader } = resolveAdsLiveDeliveryReader(opts, project.name);
|
|
30615
|
+
const verifyAdsAccount = opts.verifyAdsAccount;
|
|
30616
|
+
if (!verifyAdsAccount) {
|
|
30617
|
+
throw validationError("Ads account verification is not configured for this deployment");
|
|
30618
|
+
}
|
|
30619
|
+
const fetchedAtMs = Date.now();
|
|
30620
|
+
const lastAttemptAtMs = liveDeliveryLastAttemptAtMs.get(project.id);
|
|
30621
|
+
const remainingIntervalMs = liveDeliveryMinIntervalMs > 0 && lastAttemptAtMs !== void 0 ? lastAttemptAtMs + liveDeliveryMinIntervalMs - fetchedAtMs : 0;
|
|
30622
|
+
if (liveDeliveryInFlight.has(project.id)) {
|
|
30623
|
+
throw quotaExceeded("OpenAI Ads live delivery reads", {
|
|
30624
|
+
reason: "read-in-flight",
|
|
30625
|
+
minIntervalMs: liveDeliveryMinIntervalMs,
|
|
30626
|
+
// A LOWER bound, and only when the interval itself still blocks. How
|
|
30627
|
+
// much longer the running walk needs is unknowable from here, so once
|
|
30628
|
+
// it has outlived the interval there is no honest number to give and
|
|
30629
|
+
// the field is absent rather than guessed.
|
|
30630
|
+
...remainingIntervalMs > 0 ? { retryAfterMs: remainingIntervalMs } : {}
|
|
30631
|
+
});
|
|
30632
|
+
}
|
|
30633
|
+
if (remainingIntervalMs > 0) {
|
|
30634
|
+
throw quotaExceeded("OpenAI Ads live delivery reads", {
|
|
30635
|
+
reason: "min-interval",
|
|
30636
|
+
minIntervalMs: liveDeliveryMinIntervalMs,
|
|
30637
|
+
retryAfterMs: remainingIntervalMs
|
|
30638
|
+
});
|
|
30639
|
+
}
|
|
30640
|
+
liveDeliveryInFlight.add(project.id);
|
|
30641
|
+
const spendLiveDeliveryAttempt = () => {
|
|
30642
|
+
if (liveDeliveryMinIntervalMs <= 0) return;
|
|
30643
|
+
pruneLiveDeliveryAttempts(fetchedAtMs);
|
|
30644
|
+
liveDeliveryLastAttemptAtMs.set(project.id, fetchedAtMs);
|
|
30645
|
+
};
|
|
30646
|
+
try {
|
|
30647
|
+
let readerCalls = 0;
|
|
30648
|
+
let truncated = false;
|
|
30649
|
+
const errors = [];
|
|
30650
|
+
const callProvider = async (surface, entityId, run) => {
|
|
30651
|
+
if (readerCalls >= ADS_LIVE_MAX_READER_CALLS) {
|
|
30652
|
+
truncated = true;
|
|
30653
|
+
return null;
|
|
30654
|
+
}
|
|
30655
|
+
readerCalls += 1;
|
|
30656
|
+
try {
|
|
30657
|
+
return await run();
|
|
30658
|
+
} catch (error) {
|
|
30659
|
+
errors.push(liveReadFailure(surface, entityId, error));
|
|
30660
|
+
return null;
|
|
30661
|
+
}
|
|
30662
|
+
};
|
|
30663
|
+
readerCalls += 1;
|
|
30664
|
+
let account;
|
|
30665
|
+
spendLiveDeliveryAttempt();
|
|
30666
|
+
try {
|
|
30667
|
+
account = await verifyAdsAccount(apiKey);
|
|
30668
|
+
} catch (error) {
|
|
30669
|
+
throw liveReadProviderError("account identity", error);
|
|
30670
|
+
}
|
|
30671
|
+
if (account.id !== connection.adAccountId) {
|
|
30672
|
+
throw validationError("The configured key belongs to a different OpenAI ad account; reconnect this project");
|
|
30673
|
+
}
|
|
30674
|
+
readerCalls += 1;
|
|
30675
|
+
let liveCampaigns;
|
|
30676
|
+
try {
|
|
30677
|
+
liveCampaigns = await reader.listCampaigns(apiKey);
|
|
30678
|
+
} catch (error) {
|
|
30679
|
+
throw liveReadProviderError("campaign list", error);
|
|
30680
|
+
}
|
|
30681
|
+
const scopedCampaigns = campaignId === void 0 ? liveCampaigns : liveCampaigns.filter((campaign) => campaign.id === campaignId);
|
|
30682
|
+
if (scopedCampaigns.length > ADS_LIVE_MAX_CAMPAIGNS) truncated = true;
|
|
30683
|
+
const walkedCampaigns = scopedCampaigns.slice(0, ADS_LIVE_MAX_CAMPAIGNS);
|
|
30684
|
+
const storedCampaignRows = app.db.select().from(adsCampaigns).where(and22(
|
|
30685
|
+
eq28(adsCampaigns.projectId, project.id),
|
|
30686
|
+
campaignId === void 0 ? void 0 : eq28(adsCampaigns.id, campaignId)
|
|
30687
|
+
)).all();
|
|
30688
|
+
const storedAdGroupRows = app.db.select().from(adsAdGroups).where(and22(
|
|
30689
|
+
eq28(adsAdGroups.projectId, project.id),
|
|
30690
|
+
campaignId === void 0 ? void 0 : eq28(adsAdGroups.campaignId, campaignId)
|
|
30691
|
+
)).all();
|
|
30692
|
+
const storedAdGroupIds = new Set(storedAdGroupRows.map((row) => row.id));
|
|
30693
|
+
const storedAdRows = app.db.select().from(adsAds).where(eq28(adsAds.projectId, project.id)).all().filter((row) => campaignId === void 0 || storedAdGroupIds.has(row.adGroupId));
|
|
30694
|
+
const storedInsightRows = app.db.select({
|
|
30695
|
+
level: adsInsightsDaily.level,
|
|
30696
|
+
entityId: adsInsightsDaily.entityId,
|
|
30697
|
+
date: adsInsightsDaily.date,
|
|
30698
|
+
impressions: adsInsightsDaily.impressions,
|
|
30699
|
+
clicks: adsInsightsDaily.clicks,
|
|
30700
|
+
spendMicros: adsInsightsDaily.spendMicros,
|
|
30701
|
+
conversions: adsInsightsDaily.conversions
|
|
30702
|
+
}).from(adsInsightsDaily).where(eq28(adsInsightsDaily.projectId, project.id)).all();
|
|
30703
|
+
const storedCampaignById = new Map(storedCampaignRows.map((row) => [row.id, row]));
|
|
30704
|
+
const storedAdGroupById = new Map(storedAdGroupRows.map((row) => [row.id, row]));
|
|
30705
|
+
const storedAdById = new Map(storedAdRows.map((row) => [row.id, row]));
|
|
30706
|
+
const storedMetricsByEntity = /* @__PURE__ */ new Map();
|
|
30707
|
+
for (const row of storedInsightRows) {
|
|
30708
|
+
const key = `${row.level}:${row.entityId}`;
|
|
30709
|
+
const bucket = storedMetricsByEntity.get(key) ?? [];
|
|
30710
|
+
bucket.push({
|
|
30711
|
+
date: row.date,
|
|
30712
|
+
impressions: row.impressions,
|
|
30713
|
+
clicks: row.clicks,
|
|
30714
|
+
spendMicros: row.spendMicros,
|
|
30715
|
+
conversions: row.conversions
|
|
30716
|
+
});
|
|
30717
|
+
storedMetricsByEntity.set(key, bucket);
|
|
30718
|
+
}
|
|
30719
|
+
const storedMetricsFor = (level, entityId) => storedMetricsByEntity.get(`${level}:${entityId}`) ?? [];
|
|
30720
|
+
const accountTimezone = account.timezone ?? "UTC";
|
|
30721
|
+
const window = liveComparisonWindow(fetchedAtMs, lookbackDays, accountTimezone);
|
|
30722
|
+
const insightsRequest = {
|
|
30723
|
+
startDate: window.startDate,
|
|
30724
|
+
fetchedAtMs,
|
|
30725
|
+
timezone: accountTimezone
|
|
30726
|
+
};
|
|
30727
|
+
const observations = [];
|
|
30728
|
+
const walkedCampaignIds = /* @__PURE__ */ new Set();
|
|
30729
|
+
const walkedAdGroupIds = /* @__PURE__ */ new Set();
|
|
30730
|
+
const walkedAdIds = /* @__PURE__ */ new Set();
|
|
30731
|
+
for (const campaign of walkedCampaigns) {
|
|
30732
|
+
walkedCampaignIds.add(campaign.id);
|
|
30733
|
+
const campaignMetrics = await callProvider(
|
|
30734
|
+
"campaign insights",
|
|
30735
|
+
campaign.id,
|
|
30736
|
+
() => reader.campaignInsights(apiKey, campaign.id, insightsRequest)
|
|
30737
|
+
);
|
|
30738
|
+
observations.push({
|
|
30739
|
+
entityType: AdsLiveEntityTypes.campaign,
|
|
30740
|
+
id: campaign.id,
|
|
30741
|
+
parentId: null,
|
|
30742
|
+
live: liveEntityState(campaign),
|
|
30743
|
+
stored: storedCampaignById.has(campaign.id) ? storedEntityState(storedCampaignById.get(campaign.id)) : null,
|
|
30744
|
+
// A metrics call that did not happen is not evidence about metrics, so
|
|
30745
|
+
// both sides stay null and no delta is claimed.
|
|
30746
|
+
liveMetrics: campaignMetrics,
|
|
30747
|
+
storedMetrics: campaignMetrics === null ? null : storedMetricsFor(AdsInsightLevels.campaign, campaign.id)
|
|
30748
|
+
});
|
|
30749
|
+
const liveAdGroups = await callProvider(
|
|
30750
|
+
"ad group list",
|
|
30751
|
+
campaign.id,
|
|
30752
|
+
() => reader.listAdGroups(apiKey, campaign.id)
|
|
30753
|
+
);
|
|
30754
|
+
if (liveAdGroups === null) continue;
|
|
30755
|
+
if (liveAdGroups.length > ADS_LIVE_MAX_AD_GROUPS_PER_CAMPAIGN) truncated = true;
|
|
30756
|
+
for (const adGroup of liveAdGroups.slice(0, ADS_LIVE_MAX_AD_GROUPS_PER_CAMPAIGN)) {
|
|
30757
|
+
walkedAdGroupIds.add(adGroup.id);
|
|
30758
|
+
const adGroupMetrics = await callProvider(
|
|
30759
|
+
"ad group insights",
|
|
30760
|
+
adGroup.id,
|
|
30761
|
+
() => reader.adGroupInsights(apiKey, adGroup.id, insightsRequest)
|
|
30762
|
+
);
|
|
30763
|
+
observations.push({
|
|
30764
|
+
entityType: AdsLiveEntityTypes.ad_group,
|
|
30765
|
+
id: adGroup.id,
|
|
30766
|
+
parentId: campaign.id,
|
|
30767
|
+
live: liveEntityState(adGroup),
|
|
30768
|
+
stored: storedAdGroupById.has(adGroup.id) ? storedEntityState(storedAdGroupById.get(adGroup.id)) : null,
|
|
30769
|
+
liveMetrics: adGroupMetrics,
|
|
30770
|
+
storedMetrics: adGroupMetrics === null ? null : storedMetricsFor(AdsInsightLevels.ad_group, adGroup.id)
|
|
30771
|
+
});
|
|
30772
|
+
const liveAds = await callProvider(
|
|
30773
|
+
"ad list",
|
|
30774
|
+
adGroup.id,
|
|
30775
|
+
() => reader.listAds(apiKey, adGroup.id)
|
|
30776
|
+
);
|
|
30777
|
+
if (liveAds === null) continue;
|
|
30778
|
+
if (liveAds.length > ADS_LIVE_MAX_ADS_PER_AD_GROUP) truncated = true;
|
|
30779
|
+
for (const ad of liveAds.slice(0, ADS_LIVE_MAX_ADS_PER_AD_GROUP)) {
|
|
30780
|
+
walkedAdIds.add(ad.id);
|
|
30781
|
+
observations.push({
|
|
30782
|
+
entityType: AdsLiveEntityTypes.ad,
|
|
30783
|
+
id: ad.id,
|
|
30784
|
+
parentId: adGroup.id,
|
|
30785
|
+
live: liveEntityState(ad),
|
|
30786
|
+
stored: storedAdById.has(ad.id) ? storedEntityState(storedAdById.get(ad.id)) : null,
|
|
30787
|
+
// The provider has no per-ad insights surface (same reason ads-sync
|
|
30788
|
+
// stores no ad-level rollups), so ads carry status evidence only.
|
|
30789
|
+
liveMetrics: null,
|
|
30790
|
+
storedMetrics: null
|
|
30791
|
+
});
|
|
30792
|
+
}
|
|
30793
|
+
}
|
|
30794
|
+
}
|
|
30795
|
+
if (!truncated && errors.length === 0) {
|
|
30796
|
+
for (const row of storedCampaignRows) {
|
|
30797
|
+
if (walkedCampaignIds.has(row.id)) continue;
|
|
30798
|
+
observations.push({
|
|
30799
|
+
entityType: AdsLiveEntityTypes.campaign,
|
|
30800
|
+
id: row.id,
|
|
30801
|
+
parentId: null,
|
|
30802
|
+
live: null,
|
|
30803
|
+
stored: storedEntityState(row),
|
|
30804
|
+
liveMetrics: null,
|
|
30805
|
+
storedMetrics: null
|
|
30806
|
+
});
|
|
30807
|
+
}
|
|
30808
|
+
for (const row of storedAdGroupRows) {
|
|
30809
|
+
if (walkedAdGroupIds.has(row.id)) continue;
|
|
30810
|
+
observations.push({
|
|
30811
|
+
entityType: AdsLiveEntityTypes.ad_group,
|
|
30812
|
+
id: row.id,
|
|
30813
|
+
parentId: row.campaignId,
|
|
30814
|
+
live: null,
|
|
30815
|
+
stored: storedEntityState(row),
|
|
30816
|
+
liveMetrics: null,
|
|
30817
|
+
storedMetrics: null
|
|
30818
|
+
});
|
|
30819
|
+
}
|
|
30820
|
+
for (const row of storedAdRows) {
|
|
30821
|
+
if (walkedAdIds.has(row.id)) continue;
|
|
30822
|
+
observations.push({
|
|
30823
|
+
entityType: AdsLiveEntityTypes.ad,
|
|
30824
|
+
id: row.id,
|
|
30825
|
+
parentId: row.adGroupId,
|
|
30826
|
+
live: null,
|
|
30827
|
+
stored: storedEntityState(row),
|
|
30828
|
+
liveMetrics: null,
|
|
30829
|
+
storedMetrics: null
|
|
30830
|
+
});
|
|
30831
|
+
}
|
|
30832
|
+
}
|
|
30833
|
+
const entities = observations.map(
|
|
30834
|
+
(observation) => buildLiveEntityComparison(observation, window)
|
|
30835
|
+
);
|
|
30836
|
+
const response = {
|
|
30837
|
+
basis: AdsLiveDeliveryBases.liveProviderRead,
|
|
30838
|
+
fetchedAt: new Date(fetchedAtMs).toISOString(),
|
|
30839
|
+
adAccountId: account.id,
|
|
30840
|
+
storedSnapshotSyncedAt: connection.lastSyncedAt ?? null,
|
|
30841
|
+
metricsWindow: { lookbackDays },
|
|
30842
|
+
bounds: {
|
|
30843
|
+
maxCampaigns: ADS_LIVE_MAX_CAMPAIGNS,
|
|
30844
|
+
maxAdGroupsPerCampaign: ADS_LIVE_MAX_AD_GROUPS_PER_CAMPAIGN,
|
|
30845
|
+
maxAdsPerAdGroup: ADS_LIVE_MAX_ADS_PER_AD_GROUP,
|
|
30846
|
+
maxReaderCalls: ADS_LIVE_MAX_READER_CALLS,
|
|
30847
|
+
readerCalls,
|
|
30848
|
+
maxPagesPerReaderCall: liveDeliveryMaxPagesPerReaderCall,
|
|
30849
|
+
maxUpstreamHttpRequests: ADS_LIVE_MAX_READER_CALLS * liveDeliveryMaxPagesPerReaderCall,
|
|
30850
|
+
truncated
|
|
30851
|
+
},
|
|
30852
|
+
entities,
|
|
30853
|
+
drift: summarizeLiveDrift(entities),
|
|
30854
|
+
errors
|
|
30855
|
+
};
|
|
30856
|
+
return response;
|
|
30857
|
+
} finally {
|
|
30858
|
+
liveDeliveryInFlight.delete(project.id);
|
|
30859
|
+
}
|
|
30860
|
+
});
|
|
30381
30861
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
30382
30862
|
const project = resolveProject(app.db, request.params.name);
|
|
30383
30863
|
const row = app.db.select().from(adsConnections).where(eq28(adsConnections.projectId, project.id)).get();
|
|
@@ -43406,7 +43886,7 @@ function serializeRun(row) {
|
|
|
43406
43886
|
return { id: row.id, projectId: row.projectId, status: row.status, provider: row.provider, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, location: row.location ?? null, totalQueries: row.totalQueries, completedQueries: row.completedQueries, failedQueries: row.failedQueries, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
|
|
43407
43887
|
}
|
|
43408
43888
|
function serializeQuery(row) {
|
|
43409
|
-
return { id: row.id, position: row.position, query: row.queryText, status: row.status, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, servedModel: row.servedModel, answerText: row.answerText, groundingSources: row.groundingSources, citedDomains: row.citedDomains, searchQueries: row.searchQueries, answerMentioned: row.answerMentioned, citationState: row.citationState, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
|
|
43889
|
+
return { id: row.id, position: row.position, query: row.queryText, status: row.status, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, servedModel: row.servedModel, answerText: row.answerText, groundingSources: row.groundingSources, citedDomains: row.citedDomains, searchQueries: row.searchQueries, namedCompetitors: row.namedCompetitors, citedCompetitorDomains: row.citedCompetitorDomains, answerMentioned: row.answerMentioned, citationState: row.citationState, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
|
|
43410
43890
|
}
|
|
43411
43891
|
|
|
43412
43892
|
// ../api-routes/src/index.ts
|
|
@@ -43527,6 +44007,9 @@ async function apiRoutes(app, opts) {
|
|
|
43527
44007
|
adsCredentialStore: opts.adsCredentialStore,
|
|
43528
44008
|
verifyAdsAccount: opts.verifyAdsAccount,
|
|
43529
44009
|
adsReader: opts.adsReader,
|
|
44010
|
+
adsLiveDeliveryReader: opts.adsLiveDeliveryReader,
|
|
44011
|
+
adsLiveDeliveryMinIntervalMs: opts.adsLiveDeliveryMinIntervalMs,
|
|
44012
|
+
adsLiveDeliveryMaxPagesPerReaderCall: opts.adsLiveDeliveryMaxPagesPerReaderCall,
|
|
43530
44013
|
onAdsSyncRequested: opts.onAdsSyncRequested,
|
|
43531
44014
|
adsOperator: opts.adsOperator,
|
|
43532
44015
|
adsReconcileSweepIntervalMs: opts.adsReconcileSweepIntervalMs,
|
|
@@ -43870,6 +44353,15 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
43870
44353
|
}
|
|
43871
44354
|
return [...overlapSet];
|
|
43872
44355
|
}
|
|
44356
|
+
function computeCitedCompetitorDomains(citedDomains, competitorDomains) {
|
|
44357
|
+
const citedCompetitors = /* @__PURE__ */ new Set();
|
|
44358
|
+
for (const citedDomain of citedDomains) {
|
|
44359
|
+
for (const competitorDomain of competitorDomains) {
|
|
44360
|
+
if (domainMatches(citedDomain, competitorDomain)) citedCompetitors.add(competitorDomain);
|
|
44361
|
+
}
|
|
44362
|
+
}
|
|
44363
|
+
return [...citedCompetitors];
|
|
44364
|
+
}
|
|
43873
44365
|
function escapeRegExp5(value) {
|
|
43874
44366
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
43875
44367
|
}
|
|
@@ -43930,7 +44422,7 @@ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, com
|
|
|
43930
44422
|
for (const pattern of candidatePatterns) {
|
|
43931
44423
|
let match;
|
|
43932
44424
|
while ((match = pattern.exec(answerText)) !== null) {
|
|
43933
|
-
const candidate = cleanCandidateName(match[1]
|
|
44425
|
+
const candidate = cleanCandidateName(match[1]);
|
|
43934
44426
|
const candidateKey = brandKeyFromText(candidate);
|
|
43935
44427
|
if (!candidateKey) continue;
|
|
43936
44428
|
if (genericKeys.has(candidateKey)) continue;
|
|
@@ -44645,6 +45137,7 @@ export {
|
|
|
44645
45137
|
classifyAiUserFetch,
|
|
44646
45138
|
determineCitationState,
|
|
44647
45139
|
computeCompetitorOverlap,
|
|
45140
|
+
computeCitedCompetitorDomains,
|
|
44648
45141
|
extractRecommendedCompetitors,
|
|
44649
45142
|
getPlaceDetails,
|
|
44650
45143
|
hashPlaceDetails,
|