@ainyc/canonry 5.0.1 → 5.1.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.
@@ -6286,7 +6286,7 @@ function nonBlank(value) {
6286
6286
  return trimmed ? trimmed : void 0;
6287
6287
  }
6288
6288
  function resolveBuildCommit(env = process.env) {
6289
- const embedded = true ? "92ebeea19d64dffdadfcfaf8dcb2696e34c8636b" : void 0;
6289
+ const embedded = true ? "2c66fea459e20ff285096090dbc6a6ee788c4c00" : void 0;
6290
6290
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6291
6291
  }
6292
6292
  function resolveInstanceIdentity(env = process.env) {
@@ -6342,6 +6342,30 @@ function buildRunCompletedProps(input) {
6342
6342
  if (input.location) props.location = input.location;
6343
6343
  return props;
6344
6344
  }
6345
+ function buildSiteAuditCompletedProps(input) {
6346
+ const props = {
6347
+ status: input.status,
6348
+ durationMs: Date.now() - input.startTime
6349
+ };
6350
+ if (input.trigger) props.trigger = input.trigger;
6351
+ const domainHash = hashDomain(input.canonicalDomain ?? null);
6352
+ if (domainHash) props.domainHash = domainHash;
6353
+ const crawl = input.crawl;
6354
+ if (!crawl) return props;
6355
+ props.complete = crawl.complete;
6356
+ if (crawl.termination) props.termination = crawl.termination;
6357
+ props.pagesDiscovered = crawl.pagesDiscovered;
6358
+ props.pagesFetched = crawl.pagesFetched;
6359
+ props.pagesAudited = crawl.pagesAudited;
6360
+ props.pagesErrored = crawl.pagesErrored;
6361
+ if (crawl.pagesAudited > 0 && typeof crawl.aggregateScore === "number" && Number.isFinite(crawl.aggregateScore)) {
6362
+ props.aggregateScore = crawl.aggregateScore;
6363
+ }
6364
+ props.pageBudget = crawl.pageBudget;
6365
+ props.checkDeadLinks = crawl.checkDeadLinks;
6366
+ if (crawl.checkDeadLinks) props.deadLinksFound = crawl.deadLinksFound;
6367
+ return props;
6368
+ }
6345
6369
 
6346
6370
  // src/provider-execution-gate.ts
6347
6371
  var ProviderExecutionGate = class {
@@ -6610,7 +6634,14 @@ var JobRunner = class {
6610
6634
  this.registry = registry;
6611
6635
  }
6612
6636
  recoverStaleRuns() {
6613
- const stale = this.db.select({ id: runs.id, projectId: runs.projectId, kind: runs.kind, status: runs.status }).from(runs).where(inArray(runs.status, ["running", "queued"])).all();
6637
+ const stale = this.db.select({
6638
+ id: runs.id,
6639
+ projectId: runs.projectId,
6640
+ kind: runs.kind,
6641
+ status: runs.status,
6642
+ trigger: runs.trigger,
6643
+ startedAt: runs.startedAt
6644
+ }).from(runs).where(inArray(runs.status, ["running", "queued"])).all();
6614
6645
  if (stale.length === 0) return;
6615
6646
  const now = (/* @__PURE__ */ new Date()).toISOString();
6616
6647
  for (const run of stale) {
@@ -6628,6 +6659,35 @@ var JobRunner = class {
6628
6659
  });
6629
6660
  if (!recovered) continue;
6630
6661
  log2.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
6662
+ if (run.kind === RunKinds["site-audit"]) this.trackRecoveredSiteAudit(run);
6663
+ }
6664
+ }
6665
+ /**
6666
+ * A crawl the process died under never reaches `executeSiteAudit`'s terminal
6667
+ * telemetry, so without this every interrupted audit vanishes from the data.
6668
+ * Emitted after the recovery transaction commits. `durationMs` runs from the
6669
+ * crawl's start to this recovery, so it includes the downtime and is an upper
6670
+ * bound; a run that was still queued has no start and reports 0.
6671
+ *
6672
+ * The source is set explicitly: recovery runs during server construction,
6673
+ * before `canonry serve` switches the process source to `cli-server`.
6674
+ */
6675
+ trackRecoveredSiteAudit(run) {
6676
+ try {
6677
+ const startedAt = run.startedAt ? Date.parse(run.startedAt) : Number.NaN;
6678
+ const project = this.db.select({ canonicalDomain: projects.canonicalDomain }).from(projects).where(eq2(projects.id, run.projectId)).get();
6679
+ trackEvent(
6680
+ "site_audit.completed",
6681
+ buildSiteAuditCompletedProps({
6682
+ status: "failed",
6683
+ startTime: Number.isFinite(startedAt) ? startedAt : Date.now(),
6684
+ trigger: run.trigger,
6685
+ canonicalDomain: project?.canonicalDomain ?? null
6686
+ }),
6687
+ { errorCode: "SERVER_RESTARTED", source: "cli-server" }
6688
+ );
6689
+ } catch (err) {
6690
+ log2.warn("telemetry.recovered-site-audit-failed", { runId: run.id, error: describeError(err) });
6631
6691
  }
6632
6692
  }
6633
6693
  async executeRun(runId, projectId, providerOverride, locationOverride) {
@@ -7149,6 +7209,7 @@ var JobRunner = class {
7149
7209
  if (existingRun.kind === "answer-visibility" && runTrigger !== "probe" && totalSnapshotsInserted > 0 && !this.hasPriorActivation(projectId, runId)) {
7150
7210
  trackEvent("activation.completed", {
7151
7211
  flowVersion: ONBOARDING_FLOW_VERSION,
7212
+ kind: "answer_visibility",
7152
7213
  status: finalStatus,
7153
7214
  providerCountBucket: bucketOnboardingCount(executionContext.providerCount),
7154
7215
  queryCountBucket: bucketOnboardingCount(executionContext.queryCount),
@@ -10031,7 +10092,7 @@ function buildDiscoveryInsightTitle(input) {
10031
10092
 
10032
10093
  // src/execute-site-audit.ts
10033
10094
  import crypto19 from "crypto";
10034
- import { and as and13, eq as eq15, sql as sql6 } from "drizzle-orm";
10095
+ import { and as and13, eq as eq15, ne as ne3, sql as sql6 } from "drizzle-orm";
10035
10096
  import { runSiteCrawl } from "@canonry/aeo-audit";
10036
10097
 
10037
10098
  // src/site-audit-root.ts
@@ -11108,10 +11169,35 @@ function deadLinkCheckedCount(edges, pages) {
11108
11169
  }
11109
11170
  return new Set([...edges].filter((edge) => edge.type === "anchor" && edge.classification === "internal" && attemptedUrls.has(edge.to)).map((edge) => edge.to)).size;
11110
11171
  }
11172
+ function trackSiteAuditOutcome(db, input) {
11173
+ try {
11174
+ const errorCode = input.status === "cancelled" ? "RUN_CANCELLED" : input.status === "failed" ? "UNKNOWN" : void 0;
11175
+ trackEvent(
11176
+ "site_audit.completed",
11177
+ buildSiteAuditCompletedProps(input),
11178
+ errorCode ? { errorCode } : void 0
11179
+ );
11180
+ const pagesAudited = input.crawl?.pagesAudited ?? 0;
11181
+ if (pagesAudited === 0 || input.trigger === RunTriggers.probe) return;
11182
+ const prior = db.select({ id: siteAuditSnapshots.id }).from(siteAuditSnapshots).where(and13(eq15(siteAuditSnapshots.projectId, input.projectId), ne3(siteAuditSnapshots.runId, input.runId))).limit(1).get();
11183
+ if (prior) return;
11184
+ trackEvent("activation.completed", {
11185
+ flowVersion: ONBOARDING_FLOW_VERSION,
11186
+ kind: "site_health",
11187
+ status: input.status,
11188
+ pagesAuditedBucket: bucketOnboardingCount(pagesAudited)
11189
+ });
11190
+ } catch (error) {
11191
+ log13.warn("telemetry.failed", { runId: input.runId, projectId: input.projectId, error: describeError(error) });
11192
+ }
11193
+ }
11111
11194
  async function executeSiteAudit(db, runId, projectId, opts = {}) {
11112
11195
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11113
11196
  const claim = db.update(runs).set({ status: "running", startedAt }).where(and13(eq15(runs.id, runId), eq15(runs.projectId, projectId), eq15(runs.status, "queued"))).run();
11114
11197
  if (claim.changes === 0) return;
11198
+ const startTime = Date.now();
11199
+ const trigger = db.select({ trigger: runs.trigger }).from(runs).where(eq15(runs.id, runId)).get()?.trigger ?? null;
11200
+ let canonicalDomain = null;
11115
11201
  const attemptNumber = (db.select({ value: sql6`coalesce(max(${siteCrawlAttempts.attemptNumber}), 0)` }).from(siteCrawlAttempts).where(eq15(siteCrawlAttempts.runId, runId)).get()?.value ?? 0) + 1;
11116
11202
  const attemptId = crypto19.randomUUID();
11117
11203
  db.insert(siteCrawlAttempts).values({
@@ -11391,6 +11477,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11391
11477
  try {
11392
11478
  const project = db.select().from(projects).where(eq15(projects.id, projectId)).get();
11393
11479
  if (!project) throw new Error(`Project not found: ${projectId}`);
11480
+ canonicalDomain = project.canonicalDomain;
11394
11481
  const homepageUrl = toHomepageUrl(project.canonicalDomain);
11395
11482
  const maxPages = clampSiteAuditLimit(opts.maxPages ?? opts.limit);
11396
11483
  const maxEdges = clampSiteAuditEdgeLimit(opts.maxEdges);
@@ -11610,6 +11697,26 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11610
11697
  throw new Error(`Site audit run was no longer running before publication: ${runId}`);
11611
11698
  }
11612
11699
  log13.info("completed", { runId, projectId, status: terminalStatus, complete: crawlSummary.complete, pages: crawlSummary.pagesObserved });
11700
+ trackSiteAuditOutcome(db, {
11701
+ runId,
11702
+ projectId,
11703
+ status: crawlSummary.complete ? "completed" : "partial",
11704
+ startTime,
11705
+ trigger,
11706
+ canonicalDomain,
11707
+ crawl: {
11708
+ complete: crawlSummary.complete,
11709
+ termination: crawlSummary.terminationReason,
11710
+ pagesDiscovered: crawlSummary.pagesDiscovered,
11711
+ pagesFetched: crawlSummary.pagesFetched,
11712
+ pagesAudited: crawlSummary.auditRollup.auditedPages,
11713
+ pagesErrored: errorCount,
11714
+ aggregateScore: crawlSummary.auditRollup.aggregateScore,
11715
+ pageBudget: maxPages,
11716
+ checkDeadLinks: opts.checkDeadLinks ?? false,
11717
+ deadLinksFound
11718
+ }
11719
+ });
11613
11720
  } catch (error) {
11614
11721
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
11615
11722
  const cancelled = isCancelled(error, opts.signal);
@@ -11628,6 +11735,14 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11628
11735
  }).where(and13(eq15(runs.id, runId), eq15(runs.status, "running"))).run();
11629
11736
  });
11630
11737
  log13.error("failed", { runId, projectId, cancelled, error: message });
11738
+ trackSiteAuditOutcome(db, {
11739
+ runId,
11740
+ projectId,
11741
+ status: cancelled ? "cancelled" : "failed",
11742
+ startTime,
11743
+ trigger,
11744
+ canonicalDomain
11745
+ });
11631
11746
  throw error;
11632
11747
  }
11633
11748
  }
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  trackCliCommandFinished,
30
30
  trackEvent,
31
31
  waitForServerRuntimeStartup
32
- } from "./chunk-GMNZNLVX.js";
32
+ } from "./chunk-ZWVXUPLP.js";
33
33
  import {
34
34
  autoSyncSkills,
35
35
  formatAutoSyncNotice
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createGoogleMarketingCredentialStore,
4
4
  createGoogleMarketingRuntime,
5
5
  createServer
6
- } from "./chunk-GMNZNLVX.js";
6
+ } from "./chunk-ZWVXUPLP.js";
7
7
  import {
8
8
  loadConfig
9
9
  } from "./chunk-MEQGGODM.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "5.0.1",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "description": "Self-hosted AI visibility (AEO) platform: track how ChatGPT, Claude, Gemini, and Perplexity cite your domain, join it with Search Console, GA4, server-side traffic, and paid media, and fix what you find through agent tools (CLI, REST, MCP). Local SQLite.",
6
6
  "keywords": [
@@ -87,29 +87,29 @@
87
87
  "tsup": "^8.5.1",
88
88
  "tsx": "^4.19.0",
89
89
  "@ainyc/canonry-web": "0.0.0",
90
- "@ainyc/canonry-api-routes": "0.0.0",
90
+ "@ainyc/canonry-api-client": "0.0.0",
91
91
  "@ainyc/canonry-config": "0.0.0",
92
92
  "@ainyc/canonry-contracts": "0.0.0",
93
- "@ainyc/canonry-api-client": "0.0.0",
93
+ "@ainyc/canonry-api-routes": "0.0.0",
94
94
  "@ainyc/canonry-db": "0.0.0",
95
95
  "@ainyc/canonry-integration-bing": "0.0.0",
96
96
  "@ainyc/canonry-integration-cloudflare-queue": "0.0.0",
97
97
  "@ainyc/canonry-integration-cloudflare-worker": "0.0.0",
98
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
98
99
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
100
+ "@ainyc/canonry-integration-google": "0.0.0",
99
101
  "@ainyc/canonry-integration-google-ads": "0.0.0",
100
102
  "@ainyc/canonry-integration-google-business-profile": "0.0.0",
101
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
102
103
  "@ainyc/canonry-integration-google-places": "0.0.0",
103
- "@ainyc/canonry-integration-google": "0.0.0",
104
104
  "@ainyc/canonry-integration-google-tag-manager": "0.0.0",
105
- "@ainyc/canonry-integration-openai-ads": "0.0.0",
106
105
  "@ainyc/canonry-integration-traffic": "0.0.0",
107
- "@ainyc/canonry-provider-cdp": "0.0.0",
106
+ "@ainyc/canonry-integration-openai-ads": "0.0.0",
108
107
  "@ainyc/canonry-integration-wordpress": "0.0.0",
108
+ "@ainyc/canonry-provider-cdp": "0.0.0",
109
109
  "@ainyc/canonry-intelligence": "0.0.0",
110
110
  "@ainyc/canonry-provider-claude": "0.0.0",
111
- "@ainyc/canonry-provider-gemini": "0.0.0",
112
111
  "@ainyc/canonry-provider-local": "0.0.0",
112
+ "@ainyc/canonry-provider-gemini": "0.0.0",
113
113
  "@ainyc/canonry-provider-openai": "0.0.0",
114
114
  "@ainyc/canonry-provider-perplexity": "0.0.0"
115
115
  },