@canonry/canonry 7.62.1 → 7.63.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +2 -1
- package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +16 -3
- package/assets/assets/{AgentSetupPage-DtPnVaeF.js → AgentSetupPage-D6lCJ1G1.js} +1 -1
- package/assets/assets/{AuditHistoryPanel-Ce_oy4OV.js → AuditHistoryPanel-DlgI3o-U.js} +1 -1
- package/assets/assets/{BacklinksPage-CrqPWRd8.js → BacklinksPage-ByiMqbSI.js} +1 -1
- package/assets/assets/{HistoryPage-5v0JM7DN.js → HistoryPage-DclVeuLO.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Bm5Mrd5a.js → MeasurementPropertyPage-DDHybtHa.js} +1 -1
- package/assets/assets/{ProjectPage-DaWUh6iw.js → ProjectPage-vBu6oWFi.js} +1 -1
- package/assets/assets/{RunsPage-CfdAR2v2.js → RunsPage-CYBJ6pXX.js} +1 -1
- package/assets/assets/{SettingsPage-BFooA7JG.js → SettingsPage-w0GmXir0.js} +1 -1
- package/assets/assets/{SiteHealthSection-CerUQgAp.js → SiteHealthSection-DDTjGZ05.js} +2 -2
- package/assets/assets/ToneBadge-MKA1xeYc.js +60 -0
- package/assets/assets/{TrafficPage-C5fhZGJ2.js → TrafficPage-hUWYv-HT.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BoqARHYz.js → TrafficSourceDetailPage-CGHgPxs8.js} +1 -1
- package/assets/assets/{account-context-D_CDYrs8.js → account-context-qX-Es0RK.js} +1 -1
- package/assets/assets/{card-C6z0XyOo.js → card-CzROANT5.js} +1 -1
- package/assets/assets/{extract-error-message-By_wcU8g.js → extract-error-message-BY1STLiX.js} +1 -1
- package/assets/assets/{format-helpers-D3Ubm8vk.js → format-helpers-BNQR1HNK.js} +1 -1
- package/assets/assets/{index-DSVFZqv6.js → index-LmB57HOs.js} +4 -4
- package/assets/assets/{query-client-BETh6SO9.js → query-client-Gz37p16p.js} +1 -1
- package/assets/assets/{react-query-DzDr4w41.js → react-query-C6DtBAYQ.js} +1 -1
- package/assets/assets/{react-sigma_core.esm.min-3kgtOWbQ.js → react-sigma_core.esm.min-CgxuyPPF.js} +1 -1
- package/assets/assets/{service-BUaUaize.js → service-DyHZWnMe.js} +1 -1
- package/assets/index.html +7 -7
- package/dist/{ads-7WUY5CVC.js → ads-FOUJNHP4.js} +3 -3
- package/dist/{agent-NOA33LVM.js → agent-UVDVH7LK.js} +7 -7
- package/dist/{agent-ask-PKQDFJ7K.js → agent-ask-UEYN2SQX.js} +3 -3
- package/dist/{agent-conversations-7HVWPSFB.js → agent-conversations-F54376Q2.js} +3 -3
- package/dist/{agent-memory-Y4M2EWKA.js → agent-memory-SOHBTJ4U.js} +3 -3
- package/dist/{agent-providers-P67FWELV.js → agent-providers-MMEK2H6V.js} +3 -3
- package/dist/{agent-transcript-C2BYWPRT.js → agent-transcript-5IJ3VAEV.js} +3 -3
- package/dist/{analytics-XHYWNNW7.js → analytics-LDWO5IW6.js} +3 -3
- package/dist/{apply-6OIWDXQP.js → apply-PSGFDJIC.js} +3 -3
- package/dist/{backfill-KQPXCUUJ.js → backfill-2BVFKQ6J.js} +9 -9
- package/dist/{backlinks-ASYIZFLQ.js → backlinks-X6DXIL2O.js} +3 -3
- package/dist/{bing-ZLKJ3MIL.js → bing-3HMFUEOO.js} +3 -3
- package/dist/{bootstrap-KMVQY5JD.js → bootstrap-ZUSSLXCI.js} +10 -10
- package/dist/{cdp-V2HX4ZLE.js → cdp-FH627HJ6.js} +7 -7
- package/dist/{chunk-2HNTNKFC.js → chunk-22ABGRB7.js} +505 -131
- package/dist/{chunk-GSHHYR24.js → chunk-2LAL5W6J.js} +3 -3
- package/dist/{chunk-LFXKB5FZ.js → chunk-32KNDPD3.js} +351 -333
- package/dist/{chunk-3B2GBWIA.js → chunk-5GMYD7UZ.js} +5 -5
- package/dist/{chunk-X3GWBK7N.js → chunk-5VAC4KQ2.js} +3 -3
- package/dist/{chunk-WKZZILBI.js → chunk-5YC6IU44.js} +2 -2
- package/dist/{chunk-5NYS3ABL.js → chunk-67262NJL.js} +2 -2
- package/dist/{chunk-7EWDQUNH.js → chunk-6ABOEN5D.js} +8 -8
- package/dist/{chunk-2LY4FY4N.js → chunk-A57HYHJ3.js} +1 -1
- package/dist/{chunk-O6VSPM5S.js → chunk-AGSQWIE4.js} +76 -29
- package/dist/{chunk-KLGHY67V.js → chunk-CWMJQ2JV.js} +4 -4
- package/dist/{chunk-U2WPZZPQ.js → chunk-EHTSCLEL.js} +1 -1
- package/dist/{chunk-IVZMXHXE.js → chunk-ERJ54U2I.js} +1 -1
- package/dist/{chunk-IALK4DQJ.js → chunk-GRWZC3GQ.js} +1 -1
- package/dist/{chunk-YQGVA64Q.js → chunk-H22RCHYH.js} +22 -6
- package/dist/{chunk-RGD5SDT3.js → chunk-HNZQVIPS.js} +1 -1
- package/dist/{chunk-UVDBUMYV.js → chunk-IDQZTPRG.js} +2 -2
- package/dist/{chunk-I3CMCTNE.js → chunk-IRRONWF7.js} +4 -4
- package/dist/{chunk-UUGT7UMF.js → chunk-JM6OFCLX.js} +4 -4
- package/dist/{chunk-B5CSCGVD.js → chunk-L2QPAUTQ.js} +2 -1
- package/dist/{chunk-CJC4AQ2M.js → chunk-LBD7YXVD.js} +2 -2
- package/dist/{chunk-SDZKHJ6W.js → chunk-LEG7F3DN.js} +20 -2
- package/dist/{chunk-MMRJUOE2.js → chunk-NOQCIXM6.js} +1 -1
- package/dist/{chunk-Q7PTBHNG.js → chunk-OIHJJHJR.js} +1 -1
- package/dist/{chunk-YBDUZBGU.js → chunk-PH7RTAWW.js} +1 -1
- package/dist/{chunk-LAILXR2F.js → chunk-QNFX7ZCT.js} +1 -1
- package/dist/{chunk-SYQLUOR7.js → chunk-SG2OPR7J.js} +1 -1
- package/dist/{chunk-DAKJBAUN.js → chunk-SN5A767O.js} +1 -1
- package/dist/{chunk-YJAN2ZDV.js → chunk-SQYYAYH6.js} +3 -3
- package/dist/{chunk-U6CNJC2Y.js → chunk-TKLE2Y6H.js} +35 -1
- package/dist/{chunk-KTQVRQDM.js → chunk-UBP3AAKA.js} +1 -1
- package/dist/{chunk-APQ5EAL2.js → chunk-UYPBR3SF.js} +2 -2
- package/dist/{chunk-AO4P75DI.js → chunk-V3QMOGZE.js} +5 -5
- package/dist/{chunk-OJVXKPIN.js → chunk-ZJTU73GW.js} +3 -3
- package/dist/{citations-NOCVQPPU.js → citations-BKWLP5S3.js} +3 -3
- package/dist/{cli-catalog-WOWYZ7Q2.js → cli-catalog-VRJS66GR.js} +3 -3
- package/dist/{cli-commands-VSS42S5I.js → cli-commands-ONFE63OB.js} +7 -7
- package/dist/cli.js +13 -13
- package/dist/{competitor-KQK66W3M.js → competitor-VELMM2TJ.js} +3 -3
- package/dist/{content-OHOQO5LP.js → content-I525MITL.js} +3 -3
- package/dist/{dashboard-W6MII62Z.js → dashboard-BC4NKAO2.js} +6 -6
- package/dist/{demo-23H25IOO.js → demo-ZRFZ5EBH.js} +1 -1
- package/dist/{demo-server-WYYWTH4H.js → demo-server-7R7NGWJA.js} +11 -11
- package/dist/{discover-557WRGEK.js → discover-E2CV5ZP7.js} +3 -3
- package/dist/{discover-eval-GGRZZHHI.js → discover-eval-ESRCWRHP.js} +3 -3
- package/dist/{doctor-D5JNJKVE.js → doctor-BQDVERII.js} +3 -3
- package/dist/{evidence-DHYYVPWW.js → evidence-NUULK5XD.js} +3 -3
- package/dist/{export-cmd-M24S3YBY.js → export-cmd-K324RTN4.js} +3 -3
- package/dist/{ga-XM6WXZNP.js → ga-PQ36UFTK.js} +3 -3
- package/dist/{gbp-M3S5XYPG.js → gbp-KTYA6KHR.js} +3 -3
- package/dist/{google-PULCSUOU.js → google-3D4E7TJX.js} +4 -4
- package/dist/{google-marketing-AA625HA4.js → google-marketing-4FRO4PRN.js} +1 -1
- package/dist/{health-cmd-LVEW6OBD.js → health-cmd-CZJOZUAQ.js} +3 -3
- package/dist/{history-32ZYGZGQ.js → history-JNH34GAM.js} +3 -3
- package/dist/index.d.ts +13 -7
- package/dist/index.js +24 -24
- package/dist/{init-VJDDRCBV.js → init-TU4VSGVU.js} +11 -11
- package/dist/{insights-RK3MJ4UM.js → insights-STYJSZBB.js} +3 -3
- package/dist/intelligence-service-MWTHUE7S.js +15 -0
- package/dist/{journeys-S4CPNZYU.js → journeys-POTSSBOM.js} +3 -3
- package/dist/{keys-WCXE7WXV.js → keys-BPCSPX2O.js} +3 -3
- package/dist/{keyword-HHVLC4HI.js → keyword-6SML434W.js} +3 -3
- package/dist/{logs-VYESWGAN.js → logs-WTKKH4UJ.js} +3 -3
- package/dist/{mcp-4LRFIOLC.js → mcp-HTAHMVRC.js} +2 -2
- package/dist/mcp.js +9 -9
- package/dist/{measurement-plan-AL3EHWRK.js → measurement-plan-LJHKKRZ7.js} +5 -5
- package/dist/{notify-LUDG5PWU.js → notify-EARS4LMG.js} +3 -3
- package/dist/{organic-evidence-CIOHMKLD.js → organic-evidence-W7ZQQKPL.js} +3 -3
- package/dist/{overview-NAZJ6PJG.js → overview-C7KQD4NR.js} +4 -4
- package/dist/{project-N3MU5V6T.js → project-YU2FIL4B.js} +3 -3
- package/dist/{query-XMI2FV6G.js → query-SYFLWHFA.js} +3 -3
- package/dist/{research-MQ6QA5TV.js → research-UOUXMFBV.js} +3 -3
- package/dist/{results-export-OFEZXCJK.js → results-export-VYPG2Z6S.js} +3 -3
- package/dist/{run-YQS3LCFT.js → run-GZD3MQGS.js} +4 -4
- package/dist/{schedule-DF6HCMXN.js → schedule-PAA2NLGF.js} +3 -3
- package/dist/{search-MRDJKVOL.js → search-6RWCUEEY.js} +3 -3
- package/dist/{serve-OCAFOFS2.js → serve-VIEGFGSK.js} +25 -25
- package/dist/{session-5OXLS7NP.js → session-JLH72BF5.js} +12 -12
- package/dist/{settings-NIF34QPH.js → settings-TTA775EJ.js} +3 -3
- package/dist/{skills-JOSNWG7B.js → skills-GA35KKOV.js} +2 -2
- package/dist/{snapshot-32DLVRIJ.js → snapshot-QVQVAMMN.js} +3 -3
- package/dist/{src-F63SHN5T.js → src-CTHVFVLF.js} +2 -2
- package/dist/{status-IERHHP4C.js → status-E7B726S6.js} +4 -4
- package/dist/{technical-aeo-D7CA3CK6.js → technical-aeo-SMPN3QDQ.js} +3 -3
- package/dist/{telemetry-DRURFCCJ.js → telemetry-CTBFPMWD.js} +4 -4
- package/dist/{tools-3IZMSHSY.js → tools-YOV2KHF2.js} +11 -11
- package/dist/{traffic-AME5NDMN.js → traffic-XACOSYYM.js} +36 -7
- package/dist/{users-4CQ6ATAG.js → users-WJC7SGRL.js} +3 -3
- package/dist/{visibility-stats-R5JBISBX.js → visibility-stats-H3ZNFV5M.js} +3 -3
- package/dist/{wordpress-GFK4GR3O.js → wordpress-72YMWF7F.js} +3 -3
- package/package.json +1 -1
- package/assets/assets/ToneBadge-DKvZcONW.js +0 -60
- package/dist/intelligence-service-UNKFCG5N.js +0 -15
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
normalizeCloudflareWorkerEvent,
|
|
10
10
|
sessionizeJourneyRequests,
|
|
11
11
|
verifyRequestSignature
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-SG2OPR7J.js";
|
|
13
13
|
import {
|
|
14
14
|
currentOutcomeAttribution,
|
|
15
15
|
getRequestContext,
|
|
16
16
|
registerRequestContext,
|
|
17
17
|
requestAuditIdentity,
|
|
18
18
|
sanitizeRequestContext
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-QNFX7ZCT.js";
|
|
20
20
|
import {
|
|
21
21
|
SentimentIdempotencyConflict,
|
|
22
22
|
SentimentRepository,
|
|
@@ -154,7 +154,7 @@ import {
|
|
|
154
154
|
userSessions,
|
|
155
155
|
users,
|
|
156
156
|
writeNegativeReviewMaxStars
|
|
157
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-HNZQVIPS.js";
|
|
158
158
|
import {
|
|
159
159
|
ADS_ACTIVATE_SCOPE,
|
|
160
160
|
ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
|
|
@@ -258,6 +258,7 @@ import {
|
|
|
258
258
|
GoogleMarketingProviders,
|
|
259
259
|
GtmConnectionStates,
|
|
260
260
|
GtmSnapshotKinds,
|
|
261
|
+
HEALTH_NO_LONGER_URGENT,
|
|
261
262
|
InsightActionGroups,
|
|
262
263
|
JOURNEY_LIST_VISITS_PER_VISITOR,
|
|
263
264
|
JOURNEY_LOOKBACK_DAYS,
|
|
@@ -514,6 +515,7 @@ import {
|
|
|
514
515
|
classifySkillFile,
|
|
515
516
|
classifySurfaceFromCategory,
|
|
516
517
|
classifyTrafficPath,
|
|
518
|
+
cloudflareQueuePullTrafficSourceConfigSchema,
|
|
517
519
|
cloudflareTrafficSourceConfigSchema,
|
|
518
520
|
cloudflareWorkerIngestRequestSchema,
|
|
519
521
|
cloudflareWorkerIngestResponseSchema,
|
|
@@ -618,6 +620,7 @@ import {
|
|
|
618
620
|
findDuplicateLocationLabels,
|
|
619
621
|
forbidden,
|
|
620
622
|
formatAiReferralClassSummary,
|
|
623
|
+
formatCalendarDay,
|
|
621
624
|
formatCanonryPluginUpdateHint,
|
|
622
625
|
formatCanonryPluginVersionMismatch,
|
|
623
626
|
formatIsoDate,
|
|
@@ -739,6 +742,7 @@ import {
|
|
|
739
742
|
isRetryableHttpError,
|
|
740
743
|
isStrictSemver,
|
|
741
744
|
isSweepProvider,
|
|
745
|
+
isTransientNetworkError,
|
|
742
746
|
isVertexGroundingRedirect,
|
|
743
747
|
isVisibilityCompareClassMetric,
|
|
744
748
|
isoDateDaysBeforeInTimeZone,
|
|
@@ -1117,6 +1121,7 @@ import {
|
|
|
1117
1121
|
trafficConnectCloudflareResponseSchema,
|
|
1118
1122
|
trafficConnectVercelRequestSchema,
|
|
1119
1123
|
trafficConnectWordpressRequestSchema,
|
|
1124
|
+
trafficDeclareGapRequestSchema,
|
|
1120
1125
|
trafficEventsResponseSchema,
|
|
1121
1126
|
trafficResetRequestSchema,
|
|
1122
1127
|
trafficSampleClassifierSchema,
|
|
@@ -1158,7 +1163,7 @@ import {
|
|
|
1158
1163
|
wordpressSchemaDeployResultDtoSchema,
|
|
1159
1164
|
wordpressSchemaStatusResultDtoSchema,
|
|
1160
1165
|
wordpressStatusDtoSchema
|
|
1161
|
-
} from "./chunk-
|
|
1166
|
+
} from "./chunk-TKLE2Y6H.js";
|
|
1162
1167
|
|
|
1163
1168
|
// ../api-routes/src/user-session.ts
|
|
1164
1169
|
import crypto2 from "crypto";
|
|
@@ -8381,16 +8386,111 @@ import { z as z2 } from "zod";
|
|
|
8381
8386
|
|
|
8382
8387
|
// ../api-routes/src/notifications/alert.ts
|
|
8383
8388
|
import { z } from "zod";
|
|
8389
|
+
|
|
8390
|
+
// ../api-routes/src/notifications/health-copy.ts
|
|
8391
|
+
var HEALTH_CHECK_LABELS = {
|
|
8392
|
+
"db.file.present": "Database missing",
|
|
8393
|
+
"config.file.present": "Config file missing",
|
|
8394
|
+
"canonry.version.current": "Canonry update available",
|
|
8395
|
+
"google.auth.connection": "Search Console sign-in",
|
|
8396
|
+
"google.auth.property-access": "Search Console access",
|
|
8397
|
+
"google.auth.redirect-uri": "Google sign-in setup",
|
|
8398
|
+
"google.auth.scopes": "Search Console permissions",
|
|
8399
|
+
"gbp.auth.connection": "Business Profile sign-in",
|
|
8400
|
+
"gbp.auth.scopes": "Business Profile permissions",
|
|
8401
|
+
"gbp.account.access": "Business Profile access",
|
|
8402
|
+
"gbp.data.recent-sync": "Business Profile sync stalled",
|
|
8403
|
+
"gbp.places.api-key": "Places API key",
|
|
8404
|
+
"gbp.reviews.access": "Review alerts limited",
|
|
8405
|
+
"gbp.reviews.delivery": "Review alert not delivered",
|
|
8406
|
+
"bing.auth.connection": "Bing sign-in",
|
|
8407
|
+
"bing.auth.site-access": "Bing site access",
|
|
8408
|
+
"wordpress.publish.connection": "WordPress publishing",
|
|
8409
|
+
"ga.auth.connection": "GA4 sign-in",
|
|
8410
|
+
"ga.data.recent-data": "GA4 data stopped",
|
|
8411
|
+
"gsc.data.recent-data": "Search Console data stopped",
|
|
8412
|
+
"report.sweeps": "Monthly report sweeps",
|
|
8413
|
+
"report.models": "Monthly report models",
|
|
8414
|
+
"report.daily-data": "Monthly report data gaps",
|
|
8415
|
+
"report.ai-referral-bursts": "AI referral spikes",
|
|
8416
|
+
"ads.auth.connection": "OpenAI ads sign-in",
|
|
8417
|
+
"ads.data.recent-sync": "OpenAI ads sync stalled",
|
|
8418
|
+
"google-ads.auth.connection": "Google Ads sign-in",
|
|
8419
|
+
"google-ads.auth.scopes": "Google Ads permissions",
|
|
8420
|
+
"google-ads.account.context": "Google Ads account",
|
|
8421
|
+
"google-ads.data.recent-snapshot": "Google Ads data stale",
|
|
8422
|
+
"gtm.auth.connection": "Tag Manager sign-in",
|
|
8423
|
+
"gtm.auth.scopes": "Tag Manager permissions",
|
|
8424
|
+
"gtm.container.context": "Tag Manager container",
|
|
8425
|
+
"gtm.data.recent-snapshot": "Tag Manager data stale",
|
|
8426
|
+
"gtm.runtime.firing": "Tags not firing",
|
|
8427
|
+
"google-ads.sync.recent-failures": "Google Ads sync failing",
|
|
8428
|
+
"gtm.sync.recent-failures": "Tag Manager sync failing",
|
|
8429
|
+
"gbp.sync.recent-failures": "Business Profile sync failing",
|
|
8430
|
+
"ga.sync.recent-failures": "GA4 sync failing",
|
|
8431
|
+
"gsc.sync.recent-failures": "Search Console sync failing",
|
|
8432
|
+
"config.providers": "AI provider keys",
|
|
8433
|
+
"config.agent-providers": "Agent provider keys",
|
|
8434
|
+
"traffic.source.connected": "Traffic sync failing",
|
|
8435
|
+
"traffic.source.recent-data": "Traffic data stopped",
|
|
8436
|
+
"traffic.source.sync-lag": "Traffic sync behind",
|
|
8437
|
+
"traffic.source.queue-backlog": "Traffic backlog",
|
|
8438
|
+
"traffic.source.worker-version": "Cloudflare worker out of date",
|
|
8439
|
+
"traffic.source.credentials": "Traffic source sign-in",
|
|
8440
|
+
"traffic.source.scopes": "Traffic source permissions",
|
|
8441
|
+
"traffic.source.cache-blindspot": "Traffic hidden by cache",
|
|
8442
|
+
"backlinks.source.connected": "Backlinks source",
|
|
8443
|
+
"content.winnability.coverage": "Content analysis incomplete",
|
|
8444
|
+
"agent.skills.installed": "Agent skills missing",
|
|
8445
|
+
"agent.skills.current": "Agent skills outdated",
|
|
8446
|
+
"agent.skills.trigger-surface": "Agent skill setup",
|
|
8447
|
+
"gsc.api.reachable": "Can't reach Search Console",
|
|
8448
|
+
"ga.api.reachable": "Can't reach GA4",
|
|
8449
|
+
"gbp.api.reachable": "Can't reach Business Profile",
|
|
8450
|
+
"google-ads.api.reachable": "Can't reach Google Ads",
|
|
8451
|
+
"gtm.api.reachable": "Can't reach Tag Manager",
|
|
8452
|
+
"bing.api.reachable": "Can't reach Bing",
|
|
8453
|
+
"ads.api.reachable": "Can't reach OpenAI ads",
|
|
8454
|
+
"site.reachability": "Website down"
|
|
8455
|
+
};
|
|
8456
|
+
var HEALTH_FIXED_LINES = {
|
|
8457
|
+
"google.auth.refresh-unreachable": "Google is reachable again.",
|
|
8458
|
+
"ga.auth.refresh-unreachable": "Google is reachable again.",
|
|
8459
|
+
"gbp.auth.refresh-unreachable": "Google is reachable again.",
|
|
8460
|
+
"traffic.sync-lag.unrecovered-skip": "The traffic gap is closed.",
|
|
8461
|
+
"traffic.source.all-errored-recently": "Traffic syncs are working again.",
|
|
8462
|
+
"traffic.queue-backlog.remaining": "The traffic backlog is cleared.",
|
|
8463
|
+
"traffic.worker-version.stale": "The Cloudflare worker is up to date.",
|
|
8464
|
+
"gbp.auth.no-connection": "Business Profile is connected."
|
|
8465
|
+
};
|
|
8466
|
+
function firstSentence(text, max2 = 180) {
|
|
8467
|
+
if (!text) return null;
|
|
8468
|
+
const end = text.search(/[.!?](?:\s|$)/u);
|
|
8469
|
+
const sentence = (end === -1 ? text : text.slice(0, end + 1)).trim();
|
|
8470
|
+
if (!sentence) return null;
|
|
8471
|
+
return sentence.length <= max2 ? sentence : `${sentence.slice(0, max2 - 1)}\u2026`;
|
|
8472
|
+
}
|
|
8473
|
+
var HEALTH_NO_SIGNAL_CODE = "health.no-signal";
|
|
8474
|
+
function plainHealthCopy(check, remediation) {
|
|
8475
|
+
if (check.alert) return check.alert;
|
|
8476
|
+
if (check.code === HEALTH_NO_SIGNAL_CODE) {
|
|
8477
|
+
return { label: "Health unknown", line: "No health check produced a result, so health is unknown, not confirmed.", next: null };
|
|
8478
|
+
}
|
|
8479
|
+
return {
|
|
8480
|
+
label: HEALTH_CHECK_LABELS[check.id] ?? "Health check failing",
|
|
8481
|
+
line: firstSentence(check.summary) ?? check.summary,
|
|
8482
|
+
next: firstSentence(remediation)
|
|
8483
|
+
};
|
|
8484
|
+
}
|
|
8485
|
+
|
|
8486
|
+
// ../api-routes/src/notifications/alert.ts
|
|
8384
8487
|
var alertSeveritySchema = z.enum(["critical", "warning", "success", "info"]);
|
|
8385
8488
|
var AlertSeverities = alertSeveritySchema.enum;
|
|
8386
8489
|
var AlertFieldLabels = {
|
|
8387
|
-
status: "Status",
|
|
8388
|
-
check: "Check",
|
|
8389
|
-
previously: "Was",
|
|
8390
8490
|
remediation: "What to do",
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8491
|
+
next: "Next",
|
|
8492
|
+
alsoOpen: "Also open",
|
|
8493
|
+
fixed: "Fixed",
|
|
8394
8494
|
event: "Event",
|
|
8395
8495
|
run: "Run",
|
|
8396
8496
|
changes: "Changes",
|
|
@@ -8401,6 +8501,7 @@ var AlertFieldLabels = {
|
|
|
8401
8501
|
replied: "Replied",
|
|
8402
8502
|
source: "Source"
|
|
8403
8503
|
};
|
|
8504
|
+
var CANONRY_ALERT_CATEGORY = "\u{1F4C8} Canonry";
|
|
8404
8505
|
var HEALTH_EVENTS = /* @__PURE__ */ new Set([
|
|
8405
8506
|
"health.degraded",
|
|
8406
8507
|
"health.recovered",
|
|
@@ -8418,58 +8519,86 @@ function bulletList(lines) {
|
|
|
8418
8519
|
return omitted > 0 ? `${body}
|
|
8419
8520
|
\u2022 \u2026and ${omitted} more` : body;
|
|
8420
8521
|
}
|
|
8522
|
+
var MAX_ALSO_OPEN = 3;
|
|
8523
|
+
function fixedLines(cleared) {
|
|
8524
|
+
const named = [...new Set(cleared.map((code) => HEALTH_FIXED_LINES[code]).filter((line) => line !== void 0))];
|
|
8525
|
+
const unnamed = cleared.filter((code) => HEALTH_FIXED_LINES[code] === void 0).length;
|
|
8526
|
+
if (unnamed === 0) return named;
|
|
8527
|
+
return [...named, `${unnamed} ${named.length > 0 ? "other " : ""}problem${unnamed === 1 ? "" : "s"} fixed.`];
|
|
8528
|
+
}
|
|
8529
|
+
function labelList(labels) {
|
|
8530
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
8531
|
+
for (const label of labels) counts2.set(label, (counts2.get(label) ?? 0) + 1);
|
|
8532
|
+
const entries = [...counts2].map(([label, count10]) => count10 > 1 ? `${label} (${count10})` : label);
|
|
8533
|
+
const shown = entries.slice(0, MAX_ALSO_OPEN);
|
|
8534
|
+
const more = entries.length - shown.length;
|
|
8535
|
+
return [...shown.map((entry) => `\u2022 ${entry}`), ...more > 0 ? [`\u2022 and ${more} more`] : []].join("\n");
|
|
8536
|
+
}
|
|
8421
8537
|
function healthView(payload) {
|
|
8422
8538
|
const { health, project } = payload;
|
|
8423
8539
|
const urgentEvent = payload.event === "health.urgent" || payload.event === "health.urgent-resolved";
|
|
8424
8540
|
const recovered = payload.event === "health.recovered" || payload.event === "health.urgent-resolved";
|
|
8425
|
-
const
|
|
8426
|
-
const
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8541
|
+
const website = health.code.startsWith("site.reachability.");
|
|
8542
|
+
const base = {
|
|
8543
|
+
category: CANONRY_ALERT_CATEGORY,
|
|
8544
|
+
url: payload.dashboardUrl,
|
|
8545
|
+
footer: project.canonicalDomain,
|
|
8546
|
+
timestamp: health.checkedAt
|
|
8547
|
+
};
|
|
8548
|
+
if (recovered) {
|
|
8549
|
+
const fixed2 = fixedLines(health.cleared ?? []);
|
|
8550
|
+
if (website) {
|
|
8551
|
+
return { ...base, severity: AlertSeverities.success, title: `${project.name}: Website is back up`, body: "The website answers again.", fields: [] };
|
|
8552
|
+
}
|
|
8553
|
+
if (urgentEvent) {
|
|
8554
|
+
const eased = health.summary.startsWith(HEALTH_NO_LONGER_URGENT);
|
|
8555
|
+
const several = (health.cleared?.length ?? 0) > 1;
|
|
8556
|
+
return {
|
|
8557
|
+
...base,
|
|
8558
|
+
severity: AlertSeverities.success,
|
|
8559
|
+
title: `${project.name}: ${eased || several ? "No longer urgent" : "Urgent problem fixed"}`,
|
|
8560
|
+
body: firstSentence(eased ? health.summary.slice(HEALTH_NO_LONGER_URGENT.length) : health.summary) ?? "The urgent problem is resolved.",
|
|
8561
|
+
fields: []
|
|
8562
|
+
};
|
|
8563
|
+
}
|
|
8564
|
+
return {
|
|
8565
|
+
...base,
|
|
8566
|
+
severity: AlertSeverities.success,
|
|
8567
|
+
title: `${project.name}: Back to normal`,
|
|
8568
|
+
body: fixed2.length > 0 ? fixed2.join(" ") : "Every check passes again.",
|
|
8569
|
+
fields: []
|
|
8570
|
+
};
|
|
8439
8571
|
}
|
|
8572
|
+
const failing = health.failing;
|
|
8440
8573
|
const added = health.added ?? [];
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
const
|
|
8450
|
-
|
|
8451
|
-
fields.push({
|
|
8452
|
-
label: `${AlertFieldLabels.cleared} (${cleared.length})`,
|
|
8453
|
-
value: bulletList(cleared)
|
|
8454
|
-
});
|
|
8455
|
-
}
|
|
8456
|
-
const newlyFailing = new Set(added);
|
|
8457
|
-
const others = health.failing.filter((check) => check.code !== health.code && !newlyFailing.has(check.code));
|
|
8574
|
+
const newest = added.map((code) => failing.find((check) => check.code === code)).find((check) => check !== void 0);
|
|
8575
|
+
const lead = newest ?? failing.find((check) => check.code === health.code) ?? failing.at(0);
|
|
8576
|
+
const copy = lead ? lead.code === health.code ? plainHealthCopy({ ...lead, summary: health.summary }, lead.remediation ?? health.remediation) : plainHealthCopy(lead, lead.remediation ?? null) : plainHealthCopy({ id: "", status: health.status, code: health.code, summary: health.summary }, health.remediation);
|
|
8577
|
+
const leadStatus = lead?.status ?? health.status;
|
|
8578
|
+
const severity = urgentEvent || leadStatus === "fail" ? AlertSeverities.critical : AlertSeverities.warning;
|
|
8579
|
+
const fields = [];
|
|
8580
|
+
if (copy.next) fields.push({ label: AlertFieldLabels.next, value: copy.next });
|
|
8581
|
+
const others = failing.filter((check) => check !== lead);
|
|
8582
|
+
const otherNew = others.filter((check) => added.includes(check.code));
|
|
8583
|
+
const standing = others.filter((check) => !added.includes(check.code));
|
|
8458
8584
|
if (others.length > 0) {
|
|
8459
8585
|
fields.push({
|
|
8460
|
-
label:
|
|
8461
|
-
value:
|
|
8586
|
+
label: AlertFieldLabels.alsoOpen,
|
|
8587
|
+
value: labelList([
|
|
8588
|
+
...otherNew.map((check) => `${plainHealthCopy(check, null).label} (new)`),
|
|
8589
|
+
...standing.map((check) => plainHealthCopy(check, null).label)
|
|
8590
|
+
])
|
|
8462
8591
|
});
|
|
8463
8592
|
}
|
|
8464
|
-
const
|
|
8593
|
+
const fixed = fixedLines(health.cleared ?? []);
|
|
8594
|
+
if (fixed.length > 0) fields.push({ label: AlertFieldLabels.fixed, value: fixed.map((line) => `\u2022 ${line}`).join("\n") });
|
|
8595
|
+
const label = website ? "Website down" : copy.label;
|
|
8465
8596
|
return {
|
|
8597
|
+
...base,
|
|
8466
8598
|
severity,
|
|
8467
|
-
title:
|
|
8468
|
-
body:
|
|
8469
|
-
fields
|
|
8470
|
-
url: payload.dashboardUrl,
|
|
8471
|
-
footer: project.canonicalDomain,
|
|
8472
|
-
timestamp: health.checkedAt
|
|
8599
|
+
title: `${project.name}: ${label}${urgentEvent ? " (urgent)" : ""}`,
|
|
8600
|
+
body: copy.line,
|
|
8601
|
+
fields
|
|
8473
8602
|
};
|
|
8474
8603
|
}
|
|
8475
8604
|
function runView(payload) {
|
|
@@ -8494,6 +8623,7 @@ function runView(payload) {
|
|
|
8494
8623
|
}
|
|
8495
8624
|
return {
|
|
8496
8625
|
severity: failed ? AlertSeverities.critical : AlertSeverities.info,
|
|
8626
|
+
category: CANONRY_ALERT_CATEGORY,
|
|
8497
8627
|
title: payload.project.name,
|
|
8498
8628
|
fields,
|
|
8499
8629
|
url: payload.dashboardUrl,
|
|
@@ -8533,6 +8663,7 @@ function reviewView(payload) {
|
|
|
8533
8663
|
fields.push(sourceField);
|
|
8534
8664
|
return {
|
|
8535
8665
|
severity: AlertSeverities.warning,
|
|
8666
|
+
category: CANONRY_ALERT_CATEGORY,
|
|
8536
8667
|
title: `${project.name}: new ${review.starRating}-star review`,
|
|
8537
8668
|
body: review.comment ? review.comment.length > 500 ? `${review.comment.slice(0, 499)}\u2026` : review.comment : "Rating only, no text.",
|
|
8538
8669
|
fields,
|
|
@@ -8543,6 +8674,7 @@ function reviewView(payload) {
|
|
|
8543
8674
|
}
|
|
8544
8675
|
return {
|
|
8545
8676
|
severity: AlertSeverities.warning,
|
|
8677
|
+
category: CANONRY_ALERT_CATEGORY,
|
|
8546
8678
|
title: `${project.name}: ${reviews.length} new negative reviews`,
|
|
8547
8679
|
body: bulletList(reviews.map((r) => {
|
|
8548
8680
|
const text = r.comment ? ` "${snippet(r.comment)}"` : "";
|
|
@@ -8564,6 +8696,7 @@ function ratingView(payload) {
|
|
|
8564
8696
|
const single = ratings.length === 1 ? ratings[0] : null;
|
|
8565
8697
|
return {
|
|
8566
8698
|
severity: AlertSeverities.warning,
|
|
8699
|
+
category: CANONRY_ALERT_CATEGORY,
|
|
8567
8700
|
title: single ? `${project.name}: Google rating fell to ${single.rating}` : `${project.name}: Google rating fell at ${ratings.length} locations`,
|
|
8568
8701
|
body: single ? line(single) : bulletList(ratings.map(line)),
|
|
8569
8702
|
fields: [
|
|
@@ -8622,7 +8755,10 @@ function renderDiscord(view) {
|
|
|
8622
8755
|
return {
|
|
8623
8756
|
username: "Canonry",
|
|
8624
8757
|
embeds: [{
|
|
8625
|
-
|
|
8758
|
+
// The category line names who is speaking; the embed's colour carries
|
|
8759
|
+
// the severity, so the title stays the problem alone.
|
|
8760
|
+
...view.category ? { author: { name: view.category } } : {},
|
|
8761
|
+
title: view.category ? view.title : `${SEVERITY_ICONS[view.severity]} ${view.title}`,
|
|
8626
8762
|
...view.body ? { description: clamp(view.body, DISCORD_MAX_DESCRIPTION) } : {},
|
|
8627
8763
|
color: DISCORD_COLORS[view.severity],
|
|
8628
8764
|
fields: view.fields.map((field) => ({
|
|
@@ -8652,6 +8788,7 @@ function renderSlack(view) {
|
|
|
8652
8788
|
text: headline,
|
|
8653
8789
|
attachments: [{
|
|
8654
8790
|
color: SLACK_COLORS[view.severity],
|
|
8791
|
+
...view.category ? { author_name: view.category } : {},
|
|
8655
8792
|
title: headline,
|
|
8656
8793
|
...absoluteLink(view.url) ? { title_link: absoluteLink(view.url) } : {},
|
|
8657
8794
|
...view.body ? { text: clamp(view.body, SLACK_MAX_TEXT) } : {},
|
|
@@ -26331,23 +26468,29 @@ async function resolveHostAddresses(hostname, resolveAddresses) {
|
|
|
26331
26468
|
}
|
|
26332
26469
|
if (isLocalhostName(hostname)) return LOCALHOST_ADDRESSES;
|
|
26333
26470
|
if (resolveAddresses) return await resolveAddresses(hostname);
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
26471
|
+
const first = await queryHostAddresses(hostname);
|
|
26472
|
+
if (first.addresses.length > 0 || !first.transient) return first.addresses;
|
|
26473
|
+
await new Promise((resolve) => setTimeout(resolve, DNS_RETRY_DELAY_MS + Math.random() * DNS_RETRY_DELAY_MS));
|
|
26474
|
+
return (await queryHostAddresses(hostname)).addresses;
|
|
26475
|
+
}
|
|
26476
|
+
var DNS_RETRY_DELAY_MS = 500;
|
|
26477
|
+
async function queryHostAddresses(hostname) {
|
|
26478
|
+
const [ipv4, ipv6] = await Promise.allSettled([dns2.resolve4(hostname), dns2.resolve6(hostname)]);
|
|
26479
|
+
const unique3 = /* @__PURE__ */ new Map();
|
|
26480
|
+
if (ipv4.status === "fulfilled") {
|
|
26481
|
+
for (const address of ipv4.value) {
|
|
26482
|
+
unique3.set(`4:${address}`, { address, family: 4 });
|
|
26341
26483
|
}
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
}
|
|
26484
|
+
}
|
|
26485
|
+
if (ipv6.status === "fulfilled") {
|
|
26486
|
+
for (const address of ipv6.value) {
|
|
26487
|
+
unique3.set(`6:${address}`, { address, family: 6 });
|
|
26346
26488
|
}
|
|
26347
|
-
return [...unique3.values()];
|
|
26348
|
-
} catch {
|
|
26349
|
-
return [];
|
|
26350
26489
|
}
|
|
26490
|
+
return {
|
|
26491
|
+
addresses: [...unique3.values()],
|
|
26492
|
+
transient: [ipv4, ipv6].some((result) => result.status === "rejected" && isTransientNetworkError(result.reason))
|
|
26493
|
+
};
|
|
26351
26494
|
}
|
|
26352
26495
|
function isBlockedAddress(address, options) {
|
|
26353
26496
|
return blockedAddressReason(address, { allowLoopback: options.allowLoopback }) !== null;
|
|
@@ -41190,7 +41333,7 @@ var routeCatalog = [
|
|
|
41190
41333
|
method: "post",
|
|
41191
41334
|
path: "/api/v1/projects/{name}/traffic/sources/{id}/reset",
|
|
41192
41335
|
summary: "Advance lastSyncedAt to NOW and clear the error state",
|
|
41193
|
-
description: "Operator recovery: advances `lastSyncedAt` to NOW, sets `status` back to `connected`, and clears `last_error`. WordPress resets also clear `last_cursor` and the pending-window marker, then record an unrecovered skip through the reset instant so the next drain cannot combine an old cursor with a new window. That prevents replay but is not a historical repair. Generic WordPress replace-mode backfill is unavailable because the plugin feed cannot prove retained coverage;
|
|
41336
|
+
description: "Operator recovery: advances `lastSyncedAt` to NOW, sets `status` back to `connected`, and clears `last_error`. WordPress resets also clear `last_cursor` and the pending-window marker, then record an unrecovered skip through the reset instant so the next drain cannot combine an old cursor with a new window. That prevents replay but is not a historical repair. Generic WordPress replace-mode backfill is unavailable because the plugin feed cannot prove retained coverage; record the skipped span with `POST /traffic/sources/{id}/declare-gap` once it cannot be recovered. Common trigger: an idle Vercel/Cloud Run source whose `lastSyncedAt` has aged past upstream retention (`request-logs` ~14d, Cloud Logging 30d) and now throws on every sync. `advanceToNow: true` is required (no implicit reset). Archived sources are rejected with 400 \u2014 re-connect them via the appropriate `traffic/connect/*` endpoint instead.",
|
|
41194
41337
|
tags: ["traffic"],
|
|
41195
41338
|
parameters: [
|
|
41196
41339
|
nameParameter,
|
|
@@ -41216,6 +41359,36 @@ var routeCatalog = [
|
|
|
41216
41359
|
404: errorResponse("Project or traffic source not found.")
|
|
41217
41360
|
}
|
|
41218
41361
|
},
|
|
41362
|
+
{
|
|
41363
|
+
method: "post",
|
|
41364
|
+
path: "/api/v1/projects/{name}/traffic/sources/{id}/declare-gap",
|
|
41365
|
+
summary: "Record a source's unrecovered skip as lost and clear it",
|
|
41366
|
+
description: "Records the span a source skipped, through `skippedThroughAt`, as data that cannot be recovered, clears the marker so the `traffic.sync-lag.unrecovered-skip` doctor check passes, and writes a `traffic.source.gap-declared` audit entry with the span. Rollups are untouched. Meant for WordPress, whose plugin feed cannot prove a historical repair, and for any source whose provider no longer retains the span; a recoverable gap should be backfilled instead. `skippedThroughAt` must repeat the current marker exactly, so a newer skip recorded since the operator looked is refused, not cleared.",
|
|
41367
|
+
tags: ["traffic"],
|
|
41368
|
+
parameters: [
|
|
41369
|
+
nameParameter,
|
|
41370
|
+
{ name: "id", in: "path", required: true, description: "Traffic source ID.", schema: stringSchema }
|
|
41371
|
+
],
|
|
41372
|
+
requestBody: {
|
|
41373
|
+
required: true,
|
|
41374
|
+
content: {
|
|
41375
|
+
"application/json": {
|
|
41376
|
+
schema: {
|
|
41377
|
+
type: "object",
|
|
41378
|
+
required: ["skippedThroughAt"],
|
|
41379
|
+
properties: {
|
|
41380
|
+
skippedThroughAt: { type: "string", description: "The source's current `skippedThroughAt`, exactly as read." }
|
|
41381
|
+
}
|
|
41382
|
+
}
|
|
41383
|
+
}
|
|
41384
|
+
}
|
|
41385
|
+
},
|
|
41386
|
+
responses: {
|
|
41387
|
+
200: jsonResponse("Gap recorded; the source no longer reports an unrecovered skip.", "TrafficSourceDetailDto"),
|
|
41388
|
+
400: errorResponse("No gap is recorded, or `skippedThroughAt` does not match the current one."),
|
|
41389
|
+
404: errorResponse("Project or traffic source not found.")
|
|
41390
|
+
}
|
|
41391
|
+
},
|
|
41219
41392
|
{
|
|
41220
41393
|
method: "get",
|
|
41221
41394
|
path: "/api/v1/projects/{name}/traffic/sources",
|
|
@@ -43073,7 +43246,17 @@ function post(value, mediaMode = "exact") {
|
|
|
43073
43246
|
const mediaSourceUrlsComplete = media.every(
|
|
43074
43247
|
(item2) => item2.mediaFormat === "PHOTO" && typeof item2.sourceUrl === "string"
|
|
43075
43248
|
);
|
|
43076
|
-
|
|
43249
|
+
const providerImageUrls = media.map((item2) => {
|
|
43250
|
+
if (item2.mediaFormat !== "PHOTO" || typeof item2.googleUrl !== "string") return null;
|
|
43251
|
+
try {
|
|
43252
|
+
const url = new URL(item2.googleUrl);
|
|
43253
|
+
return url.protocol === "https:" && /^lh\d+\.googleusercontent\.com$/u.test(url.hostname) && !url.username && !url.password && !url.port && !url.hash ? url.href : null;
|
|
43254
|
+
} catch {
|
|
43255
|
+
return null;
|
|
43256
|
+
}
|
|
43257
|
+
});
|
|
43258
|
+
const providerImagesComplete = providerImageUrls.length > 0 && providerImageUrls.every((url) => url !== null);
|
|
43259
|
+
if (!mediaSourceUrlsComplete && !providerImagesComplete && mediaMode === "exact" && raw.state !== GbpLocalPostStates.REJECTED)
|
|
43077
43260
|
throw new GbpLocalPostResponseError("UNSUPPORTED_MEDIA");
|
|
43078
43261
|
const parsed = gbpLocalPostDtoSchema.safeParse({
|
|
43079
43262
|
name: raw.name,
|
|
@@ -43084,7 +43267,8 @@ function post(value, mediaMode = "exact") {
|
|
|
43084
43267
|
actionType: action?.actionType ?? null,
|
|
43085
43268
|
articleUrl: action?.url ?? null,
|
|
43086
43269
|
imageUrls: mediaSourceUrlsComplete ? media.map((item2) => item2.sourceUrl) : [],
|
|
43087
|
-
...mediaMode === "discovery" ? { mediaSourceUrlsComplete } : {},
|
|
43270
|
+
...mediaMode === "discovery" || !mediaSourceUrlsComplete ? { mediaSourceUrlsComplete } : {},
|
|
43271
|
+
...providerImagesComplete ? { providerImageUrls } : {},
|
|
43088
43272
|
createTime: raw.createTime,
|
|
43089
43273
|
updateTime: raw.updateTime,
|
|
43090
43274
|
searchUrl: raw.searchUrl ?? null
|
|
@@ -43593,11 +43777,16 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
43593
43777
|
function escapeRegExp2(str) {
|
|
43594
43778
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
43595
43779
|
}
|
|
43780
|
+
var TOKEN_REFRESH_RETRY = {
|
|
43781
|
+
maxRetries: 1,
|
|
43782
|
+
isRetryable: isTransientNetworkError,
|
|
43783
|
+
computeDelayMs: (_attempt, _err, jitteredMs) => 500 + jitteredMs
|
|
43784
|
+
};
|
|
43596
43785
|
async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
43597
43786
|
validateClientId(clientId);
|
|
43598
43787
|
validateClientSecret(clientSecret);
|
|
43599
43788
|
validateRefreshToken(currentRefreshToken);
|
|
43600
|
-
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
43789
|
+
const res = await withRetry(() => fetch(GOOGLE_TOKEN_URL, {
|
|
43601
43790
|
method: "POST",
|
|
43602
43791
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
43603
43792
|
body: new URLSearchParams({
|
|
@@ -43607,7 +43796,7 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
|
43607
43796
|
grant_type: "refresh_token"
|
|
43608
43797
|
}),
|
|
43609
43798
|
signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
|
|
43610
|
-
});
|
|
43799
|
+
}), TOKEN_REFRESH_RETRY);
|
|
43611
43800
|
if (res.status === 429) {
|
|
43612
43801
|
throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
|
|
43613
43802
|
}
|
|
@@ -61998,6 +62187,9 @@ var CLOUDFLARE_QUEUE_RETENTION_READ_TIMEOUT_MS = 1e4;
|
|
|
61998
62187
|
var CLOUDFLARE_QUEUE_VISIBILITY_TIMEOUT_MS = 5 * 6e4;
|
|
61999
62188
|
var CLOUDFLARE_QUEUE_SYNC_LEASE_TTL_MS = 5 * 6e4;
|
|
62000
62189
|
var WORDPRESS_SYNC_LEASE_TTL_MS = 5 * 6e4;
|
|
62190
|
+
var PULL_SYNC_LEASE_TTL_MS = 10 * 6e4;
|
|
62191
|
+
var TRAFFIC_BACKFILL_LEASE_TTL_MS = 5 * 6e4;
|
|
62192
|
+
var TRAFFIC_BACKFILL_LEASE_RENEW_MS = 6e4;
|
|
62001
62193
|
var DEFAULT_CLOUDFLARE_INGEST_RATE_LIMIT_MAX = 6e3;
|
|
62002
62194
|
var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/i;
|
|
62003
62195
|
function sha256Hex3(value) {
|
|
@@ -62344,6 +62536,9 @@ function isAuthoritativeTrafficSource(db, source) {
|
|
|
62344
62536
|
function trafficConnectStatus(tx, projectId, existingSource) {
|
|
62345
62537
|
return existingSource?.status === TrafficSourceStatuses.paused || hasConnectedTrafficSourceSibling(tx, projectId, existingSource?.id ?? "") ? TrafficSourceStatuses.paused : TrafficSourceStatuses.connected;
|
|
62346
62538
|
}
|
|
62539
|
+
function isSameBackfillSource(current, started, leaseOwner) {
|
|
62540
|
+
return current.syncLeaseOwner === leaseOwner && isSameTrafficSourceGeneration(current, started);
|
|
62541
|
+
}
|
|
62347
62542
|
function isSameTrafficSourceGeneration(current, started) {
|
|
62348
62543
|
return isDeepStrictEqual(current.configJson, started.configJson) && current.updatedAt === started.updatedAt && current.lastSyncedAt === started.lastSyncedAt && current.lastCursor === started.lastCursor && current.wordpressPendingUntil === started.wordpressPendingUntil;
|
|
62349
62544
|
}
|
|
@@ -62397,6 +62592,42 @@ function vercelRetentionClampError(requestedStartMs, effectiveStartMs) {
|
|
|
62397
62592
|
return new VercelRetentionClampError(requestedStartMs, effectiveStartMs);
|
|
62398
62593
|
}
|
|
62399
62594
|
async function runBackfillTask(options) {
|
|
62595
|
+
const { app, sourceRow: sourceRow2, leaseOwner } = options;
|
|
62596
|
+
const stopped = new AbortController();
|
|
62597
|
+
const releaseLease = () => {
|
|
62598
|
+
try {
|
|
62599
|
+
releaseTrafficSyncLease({ db: app.db, sourceId: sourceRow2.id, owner: leaseOwner, now: (/* @__PURE__ */ new Date()).toISOString() });
|
|
62600
|
+
} catch {
|
|
62601
|
+
}
|
|
62602
|
+
};
|
|
62603
|
+
const renewLease2 = setInterval(() => {
|
|
62604
|
+
try {
|
|
62605
|
+
const latest = app.db.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
62606
|
+
if (!latest || !isAuthoritativeTrafficSource(app.db, latest) || !isSameTrafficSourceGeneration(latest, sourceRow2)) {
|
|
62607
|
+
clearInterval(renewLease2);
|
|
62608
|
+
stopped.abort();
|
|
62609
|
+
releaseLease();
|
|
62610
|
+
return;
|
|
62611
|
+
}
|
|
62612
|
+
tryClaimTrafficSyncLease({
|
|
62613
|
+
db: app.db,
|
|
62614
|
+
sourceId: sourceRow2.id,
|
|
62615
|
+
owner: leaseOwner,
|
|
62616
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
62617
|
+
ttlMs: TRAFFIC_BACKFILL_LEASE_TTL_MS
|
|
62618
|
+
});
|
|
62619
|
+
} catch {
|
|
62620
|
+
}
|
|
62621
|
+
}, options.leaseRenewMs);
|
|
62622
|
+
renewLease2.unref?.();
|
|
62623
|
+
try {
|
|
62624
|
+
await runBackfillTaskHoldingLease(options, stopped.signal);
|
|
62625
|
+
} finally {
|
|
62626
|
+
clearInterval(renewLease2);
|
|
62627
|
+
releaseLease();
|
|
62628
|
+
}
|
|
62629
|
+
}
|
|
62630
|
+
async function runBackfillTaskHoldingLease(options, signal) {
|
|
62400
62631
|
const {
|
|
62401
62632
|
app,
|
|
62402
62633
|
runId,
|
|
@@ -62406,7 +62637,8 @@ async function runBackfillTask(options) {
|
|
|
62406
62637
|
windowEnd,
|
|
62407
62638
|
journeyKeySecret,
|
|
62408
62639
|
pullForBackfill,
|
|
62409
|
-
pullErrorPrefix
|
|
62640
|
+
pullErrorPrefix,
|
|
62641
|
+
leaseOwner
|
|
62410
62642
|
} = options;
|
|
62411
62643
|
const backfillStartedAt = Date.now();
|
|
62412
62644
|
const target = { feature: "server_traffic", operation: "backfill", attribution: currentOutcomeAttribution() };
|
|
@@ -62420,7 +62652,7 @@ async function runBackfillTask(options) {
|
|
|
62420
62652
|
app.db.transaction((tx) => {
|
|
62421
62653
|
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq80(runs.id, runId)).run();
|
|
62422
62654
|
const latestSource = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
62423
|
-
if (!preserveSourceState && latestSource && isAuthoritativeTrafficSource(tx, latestSource) &&
|
|
62655
|
+
if (!preserveSourceState && latestSource && isAuthoritativeTrafficSource(tx, latestSource) && isSameBackfillSource(latestSource, sourceRow2, leaseOwner)) {
|
|
62424
62656
|
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq80(trafficSources.id, sourceRow2.id)).run();
|
|
62425
62657
|
}
|
|
62426
62658
|
});
|
|
@@ -62428,14 +62660,19 @@ async function runBackfillTask(options) {
|
|
|
62428
62660
|
}
|
|
62429
62661
|
};
|
|
62430
62662
|
if (sourceRow2.sourceType === TrafficSourceTypes.wordpress) {
|
|
62431
|
-
markFailed(
|
|
62663
|
+
markFailed(`Generic WordPress replace backfill is unavailable because retained coverage is unproven. Record a span that cannot be recovered with \`canonry traffic declare-gap ${project.name} --source ${sourceRow2.id}\`.`);
|
|
62432
62664
|
reportBackfill({ status: OutcomeStatuses.failed, reasonCode: OutcomeReasonCodes.UNSUPPORTED });
|
|
62433
62665
|
return;
|
|
62434
62666
|
}
|
|
62435
62667
|
let allEvents;
|
|
62436
62668
|
try {
|
|
62437
|
-
allEvents = await pullForBackfill();
|
|
62669
|
+
allEvents = await pullForBackfill(signal);
|
|
62438
62670
|
} catch (e) {
|
|
62671
|
+
if (signal.aborted) {
|
|
62672
|
+
markFailed("Backfill stopped: the traffic source was reset, reconnected or deactivated while it ran", true);
|
|
62673
|
+
reportBackfill({ status: OutcomeStatuses.failed, reasonCode: OutcomeReasonCodes.NOT_CONNECTED });
|
|
62674
|
+
return;
|
|
62675
|
+
}
|
|
62439
62676
|
markFailed(
|
|
62440
62677
|
`${pullErrorPrefix}: ${describeError(e)}`,
|
|
62441
62678
|
e instanceof VercelRetentionClampError
|
|
@@ -62449,7 +62686,7 @@ async function runBackfillTask(options) {
|
|
|
62449
62686
|
try {
|
|
62450
62687
|
const completed = app.db.transaction((tx) => {
|
|
62451
62688
|
const latestSource = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
62452
|
-
const stillAuthoritative = latestSource && isAuthoritativeTrafficSource(tx, latestSource) &&
|
|
62689
|
+
const stillAuthoritative = latestSource && isAuthoritativeTrafficSource(tx, latestSource) && isSameBackfillSource(latestSource, sourceRow2, leaseOwner);
|
|
62453
62690
|
if (stillAuthoritative) {
|
|
62454
62691
|
enforceRawEventSampleRetention(tx, sourceRow2.id, finishedAt2);
|
|
62455
62692
|
}
|
|
@@ -62492,7 +62729,7 @@ async function runBackfillTask(options) {
|
|
|
62492
62729
|
}).where(eq80(runs.id, runId)).run();
|
|
62493
62730
|
return "wordpress-backfill-unsupported";
|
|
62494
62731
|
}
|
|
62495
|
-
if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !
|
|
62732
|
+
if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !isSameBackfillSource(latestSource, sourceRow2, leaseOwner)) {
|
|
62496
62733
|
tx.update(runs).set({
|
|
62497
62734
|
status: RunStatuses.failed,
|
|
62498
62735
|
error: "Traffic source was deactivated or reconfigured during backfill",
|
|
@@ -64016,15 +64253,19 @@ async function trafficRoutes(app, opts) {
|
|
|
64016
64253
|
if (sourceRow2.status === TrafficSourceStatuses.paused || sourceRow2.status === TrafficSourceStatuses.archived || hasConnectedSibling) {
|
|
64017
64254
|
throw validationError("Traffic source must be active before it can sync");
|
|
64018
64255
|
}
|
|
64019
|
-
const
|
|
64020
|
-
|
|
64256
|
+
const isWordpressSource = sourceRow2.sourceType === TrafficSourceTypes.wordpress;
|
|
64257
|
+
const syncLeaseOwner = crypto52.randomUUID();
|
|
64258
|
+
if (!tryClaimTrafficSyncLease({
|
|
64021
64259
|
db: app.db,
|
|
64022
64260
|
sourceId: sourceRow2.id,
|
|
64023
|
-
owner:
|
|
64261
|
+
owner: syncLeaseOwner,
|
|
64024
64262
|
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
64025
|
-
ttlMs: WORDPRESS_SYNC_LEASE_TTL_MS
|
|
64263
|
+
ttlMs: isWordpressSource ? WORDPRESS_SYNC_LEASE_TTL_MS : PULL_SYNC_LEASE_TTL_MS
|
|
64026
64264
|
})) {
|
|
64027
|
-
throw operationInProgress(
|
|
64265
|
+
throw operationInProgress(
|
|
64266
|
+
isWordpressSource ? "WordPress source sync is already in progress" : "A sync or backfill is already running for this traffic source",
|
|
64267
|
+
{ sourceId: sourceRow2.id }
|
|
64268
|
+
);
|
|
64028
64269
|
}
|
|
64029
64270
|
try {
|
|
64030
64271
|
const syncWindowEnd = /* @__PURE__ */ new Date();
|
|
@@ -64047,7 +64288,7 @@ async function trafficRoutes(app, opts) {
|
|
|
64047
64288
|
app.db.transaction((tx) => {
|
|
64048
64289
|
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq80(runs.id, runId)).run();
|
|
64049
64290
|
const latestSource = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
64050
|
-
if (latestSource?.status === TrafficSourceStatuses.connected && isSameTrafficSourceGeneration(latestSource, sourceRow2) &&
|
|
64291
|
+
if (latestSource?.status === TrafficSourceStatuses.connected && isSameTrafficSourceGeneration(latestSource, sourceRow2) && latestSource.syncLeaseOwner === syncLeaseOwner) {
|
|
64051
64292
|
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq80(trafficSources.id, sourceRow2.id)).run();
|
|
64052
64293
|
}
|
|
64053
64294
|
});
|
|
@@ -64068,11 +64309,11 @@ async function trafficRoutes(app, opts) {
|
|
|
64068
64309
|
}
|
|
64069
64310
|
};
|
|
64070
64311
|
const renewWordpressLease = () => {
|
|
64071
|
-
if (!
|
|
64312
|
+
if (!isWordpressSource) return;
|
|
64072
64313
|
if (!tryClaimTrafficSyncLease({
|
|
64073
64314
|
db: app.db,
|
|
64074
64315
|
sourceId: sourceRow2.id,
|
|
64075
|
-
owner:
|
|
64316
|
+
owner: syncLeaseOwner,
|
|
64076
64317
|
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
64077
64318
|
ttlMs: WORDPRESS_SYNC_LEASE_TTL_MS
|
|
64078
64319
|
})) {
|
|
@@ -64177,7 +64418,7 @@ async function trafficRoutes(app, opts) {
|
|
|
64177
64418
|
const reservedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
64178
64419
|
const reservedSource = app.db.transaction((tx) => {
|
|
64179
64420
|
const latestSource = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
64180
|
-
if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !isSameTrafficSourceGeneration(latestSource, sourceRow2) || latestSource.syncLeaseOwner !==
|
|
64421
|
+
if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !isSameTrafficSourceGeneration(latestSource, sourceRow2) || latestSource.syncLeaseOwner !== syncLeaseOwner || latestSource.lastCursor || latestSource.wordpressPendingUntil) return void 0;
|
|
64181
64422
|
tx.update(trafficSources).set({
|
|
64182
64423
|
lastSyncedAt: windowStartIso2,
|
|
64183
64424
|
wordpressPendingUntil: windowEndIso2,
|
|
@@ -64361,7 +64602,7 @@ async function trafficRoutes(app, opts) {
|
|
|
64361
64602
|
const commitOutcome = app.db.transaction((tx) => {
|
|
64362
64603
|
const latestRow = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
64363
64604
|
const latestHasConnectedSibling = tx.select().from(trafficSources).where(eq80(trafficSources.projectId, project.id)).all().some((row) => row.id !== sourceRow2.id && row.status === TrafficSourceStatuses.connected);
|
|
64364
|
-
if (!latestRow || latestRow.status !== TrafficSourceStatuses.connected && latestRow.status !== TrafficSourceStatuses.error || latestHasConnectedSibling ||
|
|
64605
|
+
if (!latestRow || latestRow.status !== TrafficSourceStatuses.connected && latestRow.status !== TrafficSourceStatuses.error || latestHasConnectedSibling || latestRow.syncLeaseOwner !== syncLeaseOwner || !isSameTrafficSourceGeneration(latestRow, sourceRow2)) {
|
|
64365
64606
|
const abortedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
64366
64607
|
tx.update(runs).set({
|
|
64367
64608
|
status: RunStatuses.failed,
|
|
@@ -64691,17 +64932,15 @@ async function trafficRoutes(app, opts) {
|
|
|
64691
64932
|
};
|
|
64692
64933
|
return response;
|
|
64693
64934
|
} finally {
|
|
64694
|
-
|
|
64695
|
-
|
|
64696
|
-
|
|
64697
|
-
|
|
64698
|
-
|
|
64699
|
-
|
|
64700
|
-
|
|
64701
|
-
|
|
64702
|
-
|
|
64703
|
-
request.log.warn({ sourceId: sourceRow2.id, error: describeError(error) }, "Failed to release WordPress sync lease");
|
|
64704
|
-
}
|
|
64935
|
+
try {
|
|
64936
|
+
releaseTrafficSyncLease({
|
|
64937
|
+
db: app.db,
|
|
64938
|
+
sourceId: sourceRow2.id,
|
|
64939
|
+
owner: syncLeaseOwner,
|
|
64940
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
64941
|
+
});
|
|
64942
|
+
} catch (error) {
|
|
64943
|
+
request.log.warn({ sourceId: sourceRow2.id, error: describeError(error) }, "Failed to release traffic sync lease");
|
|
64705
64944
|
}
|
|
64706
64945
|
}
|
|
64707
64946
|
});
|
|
@@ -64713,7 +64952,7 @@ async function trafficRoutes(app, opts) {
|
|
|
64713
64952
|
}
|
|
64714
64953
|
if (sourceRow2.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow2.sourceType !== TrafficSourceTypes.wordpress && sourceRow2.sourceType !== TrafficSourceTypes.vercel) {
|
|
64715
64954
|
throw validationError(
|
|
64716
|
-
`Backfill for source type "${sourceRow2.sourceType}" is not implemented yet \u2014 generic replace backfill supports only cloud-run and vercel; WordPress
|
|
64955
|
+
`Backfill for source type "${sourceRow2.sourceType}" is not implemented yet \u2014 generic replace backfill supports only cloud-run and vercel; a WordPress gap is recorded with \`canonry traffic declare-gap\`.`
|
|
64717
64956
|
);
|
|
64718
64957
|
}
|
|
64719
64958
|
if (!isAuthoritativeTrafficSource(app.db, sourceRow2)) {
|
|
@@ -64721,7 +64960,7 @@ async function trafficRoutes(app, opts) {
|
|
|
64721
64960
|
}
|
|
64722
64961
|
if (sourceRow2.sourceType === TrafficSourceTypes.wordpress) {
|
|
64723
64962
|
throw validationError(
|
|
64724
|
-
|
|
64963
|
+
`Generic WordPress replace backfill is unavailable because retained coverage is unproven. Record a span that cannot be recovered with \`canonry traffic declare-gap ${project.name} --source ${sourceRow2.id}\`.`
|
|
64725
64964
|
);
|
|
64726
64965
|
}
|
|
64727
64966
|
const requestedDays = request.body?.days ?? DEFAULT_BACKFILL_DAYS;
|
|
@@ -64787,11 +65026,12 @@ async function trafficRoutes(app, opts) {
|
|
|
64787
65026
|
const vercelTeamId = config.teamId ?? credential.teamId;
|
|
64788
65027
|
const vercelEnvironment = config.environment ?? credential.environment;
|
|
64789
65028
|
pullErrorPrefix = "Vercel pull failed";
|
|
64790
|
-
pullForBackfill = async () => {
|
|
65029
|
+
pullForBackfill = async (signal) => {
|
|
64791
65030
|
const collected = [];
|
|
64792
65031
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
64793
65032
|
const backfillEndMs = windowEnd.getTime();
|
|
64794
65033
|
for (let chunkStartMs = windowStart.getTime(); chunkStartMs < backfillEndMs; chunkStartMs += VERCEL_BACKFILL_CHUNK_MS) {
|
|
65034
|
+
signal.throwIfAborted();
|
|
64795
65035
|
const chunkEndMs = Math.min(chunkStartMs + VERCEL_BACKFILL_CHUNK_MS, backfillEndMs);
|
|
64796
65036
|
const drained = await drainVercelTrafficEvents({
|
|
64797
65037
|
pull: pullVercelEvents,
|
|
@@ -64819,25 +65059,42 @@ async function trafficRoutes(app, opts) {
|
|
|
64819
65059
|
}
|
|
64820
65060
|
const startedAt = windowEnd.toISOString();
|
|
64821
65061
|
const runId = crypto52.randomUUID();
|
|
64822
|
-
|
|
64823
|
-
|
|
64824
|
-
|
|
64825
|
-
|
|
64826
|
-
|
|
64827
|
-
|
|
64828
|
-
|
|
64829
|
-
|
|
64830
|
-
|
|
64831
|
-
|
|
64832
|
-
|
|
64833
|
-
|
|
64834
|
-
|
|
64835
|
-
|
|
64836
|
-
|
|
64837
|
-
|
|
65062
|
+
const leaseOwner = crypto52.randomUUID();
|
|
65063
|
+
if (!tryClaimTrafficSyncLease({
|
|
65064
|
+
db: app.db,
|
|
65065
|
+
sourceId: sourceRow2.id,
|
|
65066
|
+
owner: leaseOwner,
|
|
65067
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65068
|
+
ttlMs: TRAFFIC_BACKFILL_LEASE_TTL_MS
|
|
65069
|
+
})) {
|
|
65070
|
+
throw operationInProgress("A sync or backfill is already running for this traffic source; retry when it finishes", { sourceId: sourceRow2.id });
|
|
65071
|
+
}
|
|
65072
|
+
try {
|
|
65073
|
+
app.db.transaction((tx) => {
|
|
65074
|
+
const latestSource = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
65075
|
+
if (!latestSource || !isAuthoritativeTrafficSource(tx, latestSource) || !isSameTrafficSourceGeneration(latestSource, sourceRow2)) {
|
|
65076
|
+
throw validationError("Traffic source must remain active and unchanged before it can backfill");
|
|
65077
|
+
}
|
|
65078
|
+
tx.insert(runs).values({
|
|
65079
|
+
id: runId,
|
|
65080
|
+
projectId: project.id,
|
|
65081
|
+
kind: RunKinds["traffic-sync"],
|
|
65082
|
+
status: RunStatuses.running,
|
|
65083
|
+
trigger: RunTriggers.backfill,
|
|
65084
|
+
sourceId: sourceRow2.id,
|
|
65085
|
+
startedAt,
|
|
65086
|
+
createdAt: startedAt
|
|
65087
|
+
}).run();
|
|
65088
|
+
});
|
|
65089
|
+
} catch (error) {
|
|
65090
|
+
releaseTrafficSyncLease({ db: app.db, sourceId: sourceRow2.id, owner: leaseOwner, now: (/* @__PURE__ */ new Date()).toISOString() });
|
|
65091
|
+
throw error;
|
|
65092
|
+
}
|
|
64838
65093
|
void runBackfillTask({
|
|
64839
65094
|
app,
|
|
64840
65095
|
runId,
|
|
65096
|
+
leaseOwner,
|
|
65097
|
+
leaseRenewMs: opts.trafficBackfillLeaseRenewMs ?? TRAFFIC_BACKFILL_LEASE_RENEW_MS,
|
|
64841
65098
|
project,
|
|
64842
65099
|
sourceRow: sourceRow2,
|
|
64843
65100
|
journeyKeySecret: opts.journeyKeySecret,
|
|
@@ -64979,6 +65236,40 @@ async function trafficRoutes(app, opts) {
|
|
|
64979
65236
|
});
|
|
64980
65237
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
64981
65238
|
}));
|
|
65239
|
+
app.post("/projects/:name/traffic/sources/:id/declare-gap", async (request) => {
|
|
65240
|
+
const project = resolveProject(app.db, request.params.name);
|
|
65241
|
+
const parsed = trafficDeclareGapRequestSchema.safeParse(request.body ?? {});
|
|
65242
|
+
if (!parsed.success) {
|
|
65243
|
+
throw validationError("`skippedThroughAt` must repeat the source's recorded skip.");
|
|
65244
|
+
}
|
|
65245
|
+
let updatedRow;
|
|
65246
|
+
app.db.transaction((tx) => {
|
|
65247
|
+
const sourceRow2 = tx.select().from(trafficSources).where(and69(eq80(trafficSources.projectId, project.id), eq80(trafficSources.id, request.params.id))).get();
|
|
65248
|
+
if (!sourceRow2) {
|
|
65249
|
+
throw notFound("traffic source", request.params.id);
|
|
65250
|
+
}
|
|
65251
|
+
if (!sourceRow2.skippedThroughAt) {
|
|
65252
|
+
throw validationError(`Traffic source "${sourceRow2.id}" has no unrecovered gap to declare.`);
|
|
65253
|
+
}
|
|
65254
|
+
if (sourceRow2.skippedThroughAt !== parsed.data.skippedThroughAt) {
|
|
65255
|
+
throw validationError(
|
|
65256
|
+
`Traffic source "${sourceRow2.id}" now records a gap through ${sourceRow2.skippedThroughAt}, not ${parsed.data.skippedThroughAt}. Look at it again before declaring it.`
|
|
65257
|
+
);
|
|
65258
|
+
}
|
|
65259
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
65260
|
+
tx.update(trafficSources).set({ skippedThroughAt: null, updatedAt: now }).where(eq80(trafficSources.id, sourceRow2.id)).run();
|
|
65261
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
65262
|
+
projectId: project.id,
|
|
65263
|
+
actor: "api",
|
|
65264
|
+
action: "traffic.source.gap-declared",
|
|
65265
|
+
entityType: "traffic_source",
|
|
65266
|
+
entityId: sourceRow2.id,
|
|
65267
|
+
diff: { skippedThroughAt: sourceRow2.skippedThroughAt, sourceType: sourceRow2.sourceType }
|
|
65268
|
+
}));
|
|
65269
|
+
updatedRow = tx.select().from(trafficSources).where(eq80(trafficSources.id, sourceRow2.id)).get();
|
|
65270
|
+
});
|
|
65271
|
+
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
65272
|
+
});
|
|
64982
65273
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
64983
65274
|
const project = resolveProject(app.db, request.params.name);
|
|
64984
65275
|
const rows = app.db.select().from(trafficSources).where(eq80(trafficSources.projectId, project.id)).orderBy(desc39(trafficSources.createdAt)).all();
|
|
@@ -66479,7 +66770,12 @@ function unreachable(step, err, details) {
|
|
|
66479
66770
|
code: step.code,
|
|
66480
66771
|
summary: `Could not reach Google to ${step.action}: ${error}`,
|
|
66481
66772
|
remediation: "Check that this host can resolve and connect to Google (DNS filtering, firewall, or proxy), then re-run. The credential was not tested.",
|
|
66482
|
-
details: { ...details, error }
|
|
66773
|
+
details: { ...details, error },
|
|
66774
|
+
alert: {
|
|
66775
|
+
label: "Can't reach Google",
|
|
66776
|
+
line: "The server couldn't reach Google, so GA4 data didn't refresh.",
|
|
66777
|
+
next: "Nothing yet. If it repeats, check the server's internet or DNS."
|
|
66778
|
+
}
|
|
66483
66779
|
};
|
|
66484
66780
|
}
|
|
66485
66781
|
async function checkServiceAccount(conn) {
|
|
@@ -67450,7 +67746,12 @@ async function resolveGbpToken(ctx) {
|
|
|
67450
67746
|
status: CheckStatuses.fail,
|
|
67451
67747
|
code: "gbp.auth.no-connection",
|
|
67452
67748
|
summary: `No Google Business Profile connection for ${ctx.project.canonicalDomain}, but ${ctx.project.name} still has selected locations.`,
|
|
67453
|
-
remediation: `Run \`canonry gbp connect ${ctx.project.name}\` to authorize
|
|
67749
|
+
remediation: `Run \`canonry gbp connect ${ctx.project.name}\` to authorize.`,
|
|
67750
|
+
alert: {
|
|
67751
|
+
label: "Business Profile not connected",
|
|
67752
|
+
line: `Business Profile isn't connected for ${ctx.project.canonicalDomain}, but locations are still selected.`,
|
|
67753
|
+
next: `Connect it with \`cnry gbp connect ${ctx.project.name}\`, or deselect its locations.`
|
|
67754
|
+
}
|
|
67454
67755
|
}
|
|
67455
67756
|
};
|
|
67456
67757
|
}
|
|
@@ -67479,7 +67780,12 @@ async function resolveGbpToken(ctx) {
|
|
|
67479
67780
|
code: "gbp.auth.refresh-unreachable",
|
|
67480
67781
|
summary: `Could not reach Google to refresh the GBP token: ${error}`,
|
|
67481
67782
|
remediation: "Check that this host can resolve and connect to Google (DNS filtering, firewall, or proxy), then re-run. The stored grant was not tested.",
|
|
67482
|
-
details: { domain: conn.domain, error }
|
|
67783
|
+
details: { domain: conn.domain, error },
|
|
67784
|
+
alert: {
|
|
67785
|
+
label: "Can't reach Google",
|
|
67786
|
+
line: "The server couldn't reach Google, so Business Profile data didn't refresh.",
|
|
67787
|
+
next: "Nothing yet. If it repeats, check the server's internet or DNS."
|
|
67788
|
+
}
|
|
67483
67789
|
}
|
|
67484
67790
|
};
|
|
67485
67791
|
}
|
|
@@ -68034,7 +68340,12 @@ async function resolveAccessToken(ctx) {
|
|
|
68034
68340
|
code: "google.auth.refresh-unreachable",
|
|
68035
68341
|
summary: `Could not reach Google to refresh the GSC token: ${error}`,
|
|
68036
68342
|
remediation: "Check that this host can resolve and connect to Google (DNS filtering, firewall, or proxy), then re-run. The stored grant was not tested.",
|
|
68037
|
-
details: { domain: conn.domain, error }
|
|
68343
|
+
details: { domain: conn.domain, error },
|
|
68344
|
+
alert: {
|
|
68345
|
+
label: "Can't reach Google",
|
|
68346
|
+
line: "The server couldn't reach Google, so Search Console data didn't refresh.",
|
|
68347
|
+
next: "Nothing yet. If it repeats, check the server's internet or DNS."
|
|
68348
|
+
}
|
|
68038
68349
|
}
|
|
68039
68350
|
};
|
|
68040
68351
|
}
|
|
@@ -68565,6 +68876,18 @@ function loadProbes(ctx) {
|
|
|
68565
68876
|
configJson: r.configJson
|
|
68566
68877
|
}));
|
|
68567
68878
|
}
|
|
68879
|
+
function unrecoveredAlert(projectName, unrecovered) {
|
|
68880
|
+
const through = unrecovered.map((m) => m.skippedThroughAt).sort().at(-1);
|
|
68881
|
+
const day = formatCalendarDay(through.slice(0, 10));
|
|
68882
|
+
const wordpress = unrecovered.filter((m) => m.sourceType === TrafficSourceTypes.wordpress);
|
|
68883
|
+
const next = unrecovered.length === 1 ? wordpress.length === 1 ? `Record it as lost with \`cnry traffic declare-gap ${projectName} --source ${unrecovered[0].id}\`.` : `Backfill it with \`cnry traffic backfill ${projectName} --source ${unrecovered[0].id} --days N\`.` : "Record each WordPress gap with `cnry traffic declare-gap`, and backfill the others.";
|
|
68884
|
+
const names = unrecovered.map((m) => m.displayName).join(", ");
|
|
68885
|
+
return {
|
|
68886
|
+
label: `Traffic gap before ${day}`,
|
|
68887
|
+
line: wordpress.length === unrecovered.length ? `A stretch of traffic before ${day} is missing for ${names}. WordPress can't replay it.` : `A stretch of traffic before ${day} is missing for ${names}.`,
|
|
68888
|
+
next
|
|
68889
|
+
};
|
|
68890
|
+
}
|
|
68568
68891
|
var sourceConnectedCheck = {
|
|
68569
68892
|
id: "traffic.source.connected",
|
|
68570
68893
|
category: CheckCategories.integrations,
|
|
@@ -68610,6 +68933,15 @@ var sourceConnectedCheck = {
|
|
|
68610
68933
|
erroredIds: errored.map((s) => s.id),
|
|
68611
68934
|
failuresInARow,
|
|
68612
68935
|
lastSyncedAt: lastDataAt
|
|
68936
|
+
},
|
|
68937
|
+
alert: recovering ? {
|
|
68938
|
+
label: "Traffic sync failed",
|
|
68939
|
+
line: `The last traffic sync for ${errored.map((s) => s.displayName).join(", ")} failed.`,
|
|
68940
|
+
next: "Nothing yet. The next sync usually recovers."
|
|
68941
|
+
} : {
|
|
68942
|
+
label: "Traffic sync failing",
|
|
68943
|
+
line: `Traffic syncs for ${errored.map((s) => s.displayName).join(", ")} failed ${failuresInARow} times in a row${lastDataAt ? `, with no new data since ${formatCalendarDay(lastDataAt.slice(0, 10))}` : ""}.`,
|
|
68944
|
+
next: `Check the source with \`cnry traffic sources ${ctx.project.name}\`, then reconnect it if needed.`
|
|
68613
68945
|
}
|
|
68614
68946
|
};
|
|
68615
68947
|
}
|
|
@@ -68999,7 +69331,7 @@ var syncLagCheck = {
|
|
|
68999
69331
|
const unrecoveredWordpress = unrecovered.filter((m) => m.sourceType === "wordpress");
|
|
69000
69332
|
const unrecoveredOtherSources = unrecovered.filter((m) => m.sourceType !== "wordpress");
|
|
69001
69333
|
const remediation = [
|
|
69002
|
-
unrecoveredWordpress.length > 0 ? "For WordPress, do not run generic replace-mode backfill: the plugin feed cannot prove it still retains every bucket in the gap.
|
|
69334
|
+
unrecoveredWordpress.length > 0 ? "For WordPress, do not run generic replace-mode backfill: the plugin feed cannot prove it still retains every bucket in the gap. Once you accept the span is lost, record it with `canonry traffic declare-gap <project> --source <id>`, which keeps existing rollups and clears this check." : null,
|
|
69003
69335
|
unrecoveredOtherSources.length > 0 ? "Recover each non-WordPress gap with `canonry traffic backfill <project> --source <id> --days N --wait`, choosing N so the window reaches back past the skipped instant and stays inside the provider's log retention. This clears only when a backfill actually covers the span." : null
|
|
69004
69336
|
].filter((part) => part !== null).join(" ");
|
|
69005
69337
|
return {
|
|
@@ -69007,7 +69339,8 @@ var syncLagCheck = {
|
|
|
69007
69339
|
code: "traffic.sync-lag.unrecovered-skip",
|
|
69008
69340
|
summary: `${unrecovered.length} traffic source(s) previously skipped traffic that has not been backfilled: ${unrecovered.map((m) => `${m.displayName} (skipped through ${m.skippedThroughAt})`).join(", ")}. Current lag may look healthy; the gap does not close on its own.`,
|
|
69009
69341
|
remediation,
|
|
69010
|
-
details
|
|
69342
|
+
details,
|
|
69343
|
+
alert: unrecoveredAlert(ctx.project.name, unrecovered)
|
|
69011
69344
|
};
|
|
69012
69345
|
}
|
|
69013
69346
|
const discarding = measured.filter((m) => m.discarding);
|
|
@@ -69066,29 +69399,33 @@ var QUEUE_NOT_VISIBLE_STOPS = [
|
|
|
69066
69399
|
CloudflareQueueDrainStopReasons["empty-pulls"],
|
|
69067
69400
|
CloudflareQueueDrainStopReasons["unleased-only"]
|
|
69068
69401
|
];
|
|
69069
|
-
function queueResidualCause(stopReasons) {
|
|
69402
|
+
function queueResidualCause(stopReasons, projectName, sourceId) {
|
|
69070
69403
|
const readRuns = "Read why each sync stopped with `canonry runs <project> --kind traffic-sync --format json` (`syncSummary.stopReason`).";
|
|
69071
69404
|
if (stopReasons.every((reason) => reason !== null && QUEUE_LIMIT_STOPS.includes(reason))) {
|
|
69072
69405
|
return {
|
|
69073
69406
|
cause: "The drain stopped at its time limit or pull cap before the Queue was empty.",
|
|
69074
|
-
remediation: "If the backlog keeps growing, messages will pass the Queue's retention and be dropped. Shorten the traffic-sync schedule interval, or raise `CANONRY_CLOUDFLARE_QUEUE_SYNC_DEADLINE_MS` (default 4 minutes per sync)."
|
|
69407
|
+
remediation: "If the backlog keeps growing, messages will pass the Queue's retention and be dropped. Shorten the traffic-sync schedule interval, or raise `CANONRY_CLOUDFLARE_QUEUE_SYNC_DEADLINE_MS` (default 4 minutes per sync).",
|
|
69408
|
+
next: "If it keeps growing, sync more often, or the oldest requests expire from the queue."
|
|
69075
69409
|
};
|
|
69076
69410
|
}
|
|
69077
69411
|
if (stopReasons.every((reason) => reason !== null && QUEUE_NOT_VISIBLE_STOPS.includes(reason))) {
|
|
69078
69412
|
return {
|
|
69079
69413
|
cause: "Cloudflare returned no more messages to pull while still counting them, so they were leased or not yet visible. The drain stopped before its time limit, so raising the limit does not help.",
|
|
69080
|
-
remediation: "Later syncs pull them as they become visible. If the count does not fall across syncs, check the Queue in the Cloudflare dashboard for in-flight or delayed messages before they pass the Queue's retention."
|
|
69414
|
+
remediation: "Later syncs pull them as they become visible. If the count does not fall across syncs, check the Queue in the Cloudflare dashboard for in-flight or delayed messages before they pass the Queue's retention.",
|
|
69415
|
+
next: "Nothing yet. Later syncs pull them once Cloudflare releases them."
|
|
69081
69416
|
};
|
|
69082
69417
|
}
|
|
69083
69418
|
if (stopReasons.every((reason) => reason === CloudflareQueueDrainStopReasons.error)) {
|
|
69084
69419
|
return {
|
|
69085
69420
|
cause: "The last sync failed before the Queue was empty.",
|
|
69086
|
-
remediation: `Fix the sync error, then run \`canonry traffic sync <project> --source <id>\`. ${readRuns}
|
|
69421
|
+
remediation: `Fix the sync error, then run \`canonry traffic sync <project> --source <id>\`. ${readRuns}`,
|
|
69422
|
+
next: `Fix the sync error, then run \`cnry traffic sync ${projectName} --source ${sourceId ?? "<id>"}\`.`
|
|
69087
69423
|
};
|
|
69088
69424
|
}
|
|
69089
69425
|
return {
|
|
69090
69426
|
cause: "The last sync stopped before the Queue was empty.",
|
|
69091
|
-
remediation: `${readRuns} If it hit its time limit, shorten the traffic-sync schedule interval or raise \`CANONRY_CLOUDFLARE_QUEUE_SYNC_DEADLINE_MS\` (default 4 minutes per sync)
|
|
69427
|
+
remediation: `${readRuns} If it hit its time limit, shorten the traffic-sync schedule interval or raise \`CANONRY_CLOUDFLARE_QUEUE_SYNC_DEADLINE_MS\` (default 4 minutes per sync).`,
|
|
69428
|
+
next: `See why the last sync stopped with \`cnry runs ${projectName} --kind traffic-sync\`.`
|
|
69092
69429
|
};
|
|
69093
69430
|
}
|
|
69094
69431
|
var queueBacklogCheck = {
|
|
@@ -69131,13 +69468,22 @@ var queueBacklogCheck = {
|
|
|
69131
69468
|
const remaining = measured.filter((source) => (source.queueBacklogCount ?? 0) > 0);
|
|
69132
69469
|
const total = remaining.reduce((sum, source) => sum + source.queueBacklogCount, 0);
|
|
69133
69470
|
if (total > CLOUDFLARE_QUEUE_RESIDUAL_WARN_COUNT) {
|
|
69134
|
-
const { cause, remediation } = queueResidualCause(
|
|
69471
|
+
const { cause, remediation, next } = queueResidualCause(
|
|
69472
|
+
remaining.map((source) => stopReasons.get(source.id) ?? null),
|
|
69473
|
+
ctx.project.name,
|
|
69474
|
+
remaining.length === 1 ? remaining[0].id : null
|
|
69475
|
+
);
|
|
69135
69476
|
return {
|
|
69136
69477
|
status: CheckStatuses.warn,
|
|
69137
69478
|
code: "traffic.queue-backlog.remaining",
|
|
69138
69479
|
summary: `${total.toLocaleString("en-US")} message(s) remained across ${remaining.length} Cloudflare Queue source(s) after the last sync, more than the ${CLOUDFLARE_QUEUE_RESIDUAL_WARN_COUNT.toLocaleString("en-US")} that can arrive while a drain runs. ${cause}`,
|
|
69139
69480
|
remediation,
|
|
69140
|
-
details
|
|
69481
|
+
details,
|
|
69482
|
+
alert: {
|
|
69483
|
+
label: "Traffic backlog",
|
|
69484
|
+
line: `${total.toLocaleString("en-US")} site requests are waiting to be processed. Normal is under ${CLOUDFLARE_QUEUE_RESIDUAL_WARN_COUNT.toLocaleString("en-US")}.`,
|
|
69485
|
+
next
|
|
69486
|
+
}
|
|
69141
69487
|
};
|
|
69142
69488
|
}
|
|
69143
69489
|
if (total > 0) {
|
|
@@ -69157,6 +69503,25 @@ var queueBacklogCheck = {
|
|
|
69157
69503
|
};
|
|
69158
69504
|
}
|
|
69159
69505
|
};
|
|
69506
|
+
function cloudflareReconnectCommand(projectName, source) {
|
|
69507
|
+
const base = `cnry traffic connect cloudflare ${projectName}`;
|
|
69508
|
+
if (!isCloudflareQueuePull(source)) return `\`${base}\``;
|
|
69509
|
+
const parsed = cloudflareQueuePullTrafficSourceConfigSchema.safeParse(source.configJson);
|
|
69510
|
+
if (!parsed.success) {
|
|
69511
|
+
return `\`${base} --delivery-mode queue-pull\` with its Queue options (\`cnry traffic sources ${projectName}\`)`;
|
|
69512
|
+
}
|
|
69513
|
+
const config = parsed.data;
|
|
69514
|
+
const options = [
|
|
69515
|
+
"--delivery-mode queue-pull",
|
|
69516
|
+
`--account-id ${config.accountId}`,
|
|
69517
|
+
`--queue-id ${config.queueId}`,
|
|
69518
|
+
`--queue-name ${config.queueName}`,
|
|
69519
|
+
`--retention-seconds ${config.retentionSeconds}`,
|
|
69520
|
+
...config.zoneId ? [`--zone-id ${config.zoneId}`] : [],
|
|
69521
|
+
"--api-token-file <path>"
|
|
69522
|
+
];
|
|
69523
|
+
return `\`${base} ${options.join(" ")}\``;
|
|
69524
|
+
}
|
|
69160
69525
|
var workerVersionCheck = {
|
|
69161
69526
|
id: "traffic.source.worker-version",
|
|
69162
69527
|
category: CheckCategories.integrations,
|
|
@@ -69165,6 +69530,7 @@ var workerVersionCheck = {
|
|
|
69165
69530
|
run: (ctx) => {
|
|
69166
69531
|
if (!ctx.project) return skippedNoProject5();
|
|
69167
69532
|
const sources = loadProbes(ctx).filter((source) => isActiveSource(source) && isCloudflareWorkerSource(source));
|
|
69533
|
+
const probeById = new Map(sources.map((source) => [source.id, source]));
|
|
69168
69534
|
if (sources.length === 0) {
|
|
69169
69535
|
return {
|
|
69170
69536
|
status: CheckStatuses.skipped,
|
|
@@ -69193,7 +69559,12 @@ var workerVersionCheck = {
|
|
|
69193
69559
|
code: "traffic.worker-version.stale",
|
|
69194
69560
|
summary: `${stale.length} Cloudflare Worker source(s) most recently reported a version that differs from the generated version.`,
|
|
69195
69561
|
remediation: "Regenerate and redeploy each source's Cloudflare Worker from the credential-owning host with `canonry traffic connect cloudflare <project>` and the source's existing delivery-mode options; then verify its route or Queue binding.",
|
|
69196
|
-
details
|
|
69562
|
+
details,
|
|
69563
|
+
alert: {
|
|
69564
|
+
label: "Cloudflare worker out of date",
|
|
69565
|
+
line: `The Cloudflare traffic worker for ${stale.map((source) => source.displayName).join(", ")} doesn't match this server's version.`,
|
|
69566
|
+
next: stale.length === 1 ? `Redeploy it with ${cloudflareReconnectCommand(ctx.project.name, probeById.get(stale[0].sourceId))}.` : `Redeploy each in its own delivery mode: ${stale.map((source) => `${source.displayName}: ${cloudflareReconnectCommand(ctx.project.name, probeById.get(source.sourceId))}`).join("; ")}.`
|
|
69567
|
+
}
|
|
69197
69568
|
};
|
|
69198
69569
|
}
|
|
69199
69570
|
const waiting = measured.filter((source) => source.state === "waiting-for-first-event");
|
|
@@ -69692,6 +70063,7 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
69692
70063
|
summary: output.summary,
|
|
69693
70064
|
remediation: output.remediation ?? null,
|
|
69694
70065
|
details: output.details,
|
|
70066
|
+
...output.alert ? { alert: output.alert } : {},
|
|
69695
70067
|
durationMs: Date.now() - checkStarted
|
|
69696
70068
|
});
|
|
69697
70069
|
}
|
|
@@ -69718,8 +70090,9 @@ function supersede(definition2, result, failedCauses) {
|
|
|
69718
70090
|
if (threw(definition2, result)) return result;
|
|
69719
70091
|
const causes = (definition2.supersededBy ?? []).filter((id) => failedCauses.has(id));
|
|
69720
70092
|
if (causes.length === 0) return result;
|
|
70093
|
+
const { alert: _symptomAlert, ...symptom } = result;
|
|
69721
70094
|
return {
|
|
69722
|
-
...
|
|
70095
|
+
...symptom,
|
|
69723
70096
|
status: CheckStatuses.skipped,
|
|
69724
70097
|
code: `${definition2.id}.superseded`,
|
|
69725
70098
|
summary: `Superseded by failing ${causes.join(", ")}, which names the cause. ${result.summary}`,
|
|
@@ -74037,6 +74410,7 @@ async function apiRoutes(app, opts) {
|
|
|
74037
74410
|
vercelTrafficCredentialStore: opts.vercelTrafficCredentialStore,
|
|
74038
74411
|
pullVercelTrafficEvents: opts.pullVercelTrafficEvents,
|
|
74039
74412
|
vercelSyncDeadlineMs: opts.vercelSyncDeadlineMs,
|
|
74413
|
+
trafficBackfillLeaseRenewMs: opts.trafficBackfillLeaseRenewMs,
|
|
74040
74414
|
cloudflareTrafficCredentialStore: opts.cloudflareTrafficCredentialStore,
|
|
74041
74415
|
pullCloudflareQueueMessages: opts.pullCloudflareQueueMessages,
|
|
74042
74416
|
ackCloudflareQueueMessages: opts.ackCloudflareQueueMessages,
|