@ainyc/canonry 4.50.0 → 4.51.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.
Files changed (29) hide show
  1. package/assets/assets/BacklinksPage-DIZCcqsP.js +1 -0
  2. package/assets/assets/ChartPrimitives-9Kx3gzQL.js +1 -0
  3. package/assets/assets/ProjectPage-R2cxJb5Y.js +6 -0
  4. package/assets/assets/RunRow-DqezNIUy.js +1 -0
  5. package/assets/assets/RunsPage-CfvTJ9Ny.js +1 -0
  6. package/assets/assets/SettingsPage-HfMGIa5v.js +1 -0
  7. package/assets/assets/TrafficPage-DV_Dvpl3.js +1 -0
  8. package/assets/assets/TrafficSourceDetailPage-lefreKBO.js +1 -0
  9. package/assets/assets/arrow-left-DpxpMUNt.js +1 -0
  10. package/assets/assets/index-DKBPD33e.js +210 -0
  11. package/assets/assets/{index-DOP3oQmw.css → index-DeGyEwik.css} +1 -1
  12. package/assets/assets/server-traffic-Bm8iKtXK.js +1 -0
  13. package/assets/assets/trash-2-CnBiLbiZ.js +1 -0
  14. package/assets/assets/{vendor-markdown-DN2199pt.js → vendor-markdown-DK7fbRNb.js} +1 -1
  15. package/assets/assets/{vendor-radix-Bvy4KXTI.js → vendor-radix-B57xfQbP.js} +1 -1
  16. package/assets/assets/{vendor-recharts-DPeiXMwW.js → vendor-recharts-DWvKDyBF.js} +1 -1
  17. package/assets/assets/vendor-tanstack-Dq7p98wZ.js +1 -0
  18. package/assets/index.html +6 -6
  19. package/dist/{chunk-QIG3TKL4.js → chunk-2ARCCG5E.js} +72 -1
  20. package/dist/{chunk-M4USKXJ4.js → chunk-DLDLDWH4.js} +128 -40
  21. package/dist/{chunk-YVP5CTSV.js → chunk-FDR3G6SB.js} +2050 -1577
  22. package/dist/{chunk-MYDJ25GO.js → chunk-GGXU5VKI.js} +111 -77
  23. package/dist/cli.js +6 -6
  24. package/dist/index.js +4 -4
  25. package/dist/{intelligence-service-TY7IPRST.js → intelligence-service-XMZEWLCW.js} +2 -2
  26. package/dist/mcp.js +2 -2
  27. package/package.json +6 -6
  28. package/assets/assets/index-DVK0M62U.js +0 -214
  29. package/assets/assets/vendor-tanstack-DPXTJtWt.js +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  categoryLabel,
11
11
  determineAnswerMentioned,
12
12
  normalizeProjectDomain
13
- } from "./chunk-QIG3TKL4.js";
13
+ } from "./chunk-2ARCCG5E.js";
14
14
 
15
15
  // src/intelligence-service.ts
16
16
  import { eq, desc, asc, and, ne, or, inArray } from "drizzle-orm";
@@ -37,6 +37,7 @@ __export(schema_exports, {
37
37
  bingUrlInspections: () => bingUrlInspections,
38
38
  ccReleaseSyncs: () => ccReleaseSyncs,
39
39
  competitors: () => competitors,
40
+ contentTargetDismissals: () => contentTargetDismissals,
40
41
  crawlerEventsHourly: () => crawlerEventsHourly,
41
42
  discoveryProbes: () => discoveryProbes,
42
43
  discoverySessions: () => discoverySessions,
@@ -58,6 +59,7 @@ __export(schema_exports, {
58
59
  queries: () => queries,
59
60
  querySnapshots: () => querySnapshots,
60
61
  rawEventSamples: () => rawEventSamples,
62
+ recommendationExplanations: () => recommendationExplanations,
61
63
  runs: () => runs,
62
64
  schedules: () => schedules,
63
65
  trafficSources: () => trafficSources,
@@ -69,16 +71,16 @@ var projects = sqliteTable("projects", {
69
71
  name: text("name").notNull().unique(),
70
72
  displayName: text("display_name").notNull(),
71
73
  canonicalDomain: text("canonical_domain").notNull(),
72
- ownedDomains: text("owned_domains").notNull().default("[]"),
73
- aliases: text("aliases").notNull().default("[]"),
74
+ ownedDomains: text("owned_domains", { mode: "json" }).$type().notNull().default([]),
75
+ aliases: text("aliases", { mode: "json" }).$type().notNull().default([]),
74
76
  country: text("country").notNull(),
75
77
  language: text("language").notNull(),
76
- tags: text("tags").notNull().default("[]"),
77
- labels: text("labels").notNull().default("{}"),
78
- providers: text("providers").notNull().default("[]"),
79
- locations: text("locations").notNull().default("[]"),
78
+ tags: text("tags", { mode: "json" }).$type().notNull().default([]),
79
+ labels: text("labels", { mode: "json" }).$type().notNull().default({}),
80
+ providers: text("providers", { mode: "json" }).$type().notNull().default([]),
81
+ locations: text("locations", { mode: "json" }).$type().notNull().default([]),
80
82
  defaultLocation: text("default_location"),
81
- autoExtractBacklinks: integer("auto_extract_backlinks").notNull().default(0),
83
+ autoExtractBacklinks: integer("auto_extract_backlinks", { mode: "boolean" }).notNull().default(false),
82
84
  configSource: text("config_source").notNull().default("cli"),
83
85
  configRevision: integer("config_revision").notNull().default(1),
84
86
  icpDescription: text("icp_description"),
@@ -112,7 +114,7 @@ var runs = sqliteTable("runs", {
112
114
  status: text("status").notNull().default("queued"),
113
115
  trigger: text("trigger").notNull().default("manual"),
114
116
  location: text("location"),
115
- queries: text("queries"),
117
+ queries: text("queries", { mode: "json" }).$type(),
116
118
  sourceId: text("source_id"),
117
119
  startedAt: text("started_at"),
118
120
  finishedAt: text("finished_at"),
@@ -139,9 +141,9 @@ var querySnapshots = sqliteTable("query_snapshots", {
139
141
  citationState: text("citation_state").notNull(),
140
142
  answerMentioned: integer("answer_mentioned", { mode: "boolean" }),
141
143
  answerText: text("answer_text"),
142
- citedDomains: text("cited_domains").notNull().default("[]"),
143
- competitorOverlap: text("competitor_overlap").notNull().default("[]"),
144
- recommendedCompetitors: text("recommended_competitors").notNull().default("[]"),
144
+ citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
145
+ competitorOverlap: text("competitor_overlap", { mode: "json" }).$type().notNull().default([]),
146
+ recommendedCompetitors: text("recommended_competitors", { mode: "json" }).$type().notNull().default([]),
145
147
  location: text("location"),
146
148
  screenshotPath: text("screenshot_path"),
147
149
  rawResponse: text("raw_response"),
@@ -177,7 +179,7 @@ var apiKeys = sqliteTable("api_keys", {
177
179
  name: text("name").notNull(),
178
180
  keyHash: text("key_hash").notNull().unique(),
179
181
  keyPrefix: text("key_prefix").notNull(),
180
- scopes: text("scopes").notNull().default('["*"]'),
182
+ scopes: text("scopes", { mode: "json" }).$type().notNull().default(["*"]),
181
183
  createdAt: text("created_at").notNull(),
182
184
  lastUsedAt: text("last_used_at"),
183
185
  revokedAt: text("revoked_at")
@@ -195,8 +197,8 @@ var schedules = sqliteTable("schedules", {
195
197
  cronExpr: text("cron_expr").notNull(),
196
198
  preset: text("preset"),
197
199
  timezone: text("timezone").notNull().default("UTC"),
198
- enabled: integer("enabled").notNull().default(1),
199
- providers: text("providers").notNull().default("[]"),
200
+ enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
201
+ providers: text("providers", { mode: "json" }).$type().notNull().default([]),
200
202
  /** Optional traffic-source UUID for traffic-sync schedules. Null for other kinds. */
201
203
  sourceId: text("source_id"),
202
204
  lastRunAt: text("last_run_at"),
@@ -210,9 +212,9 @@ var notifications = sqliteTable("notifications", {
210
212
  id: text("id").primaryKey(),
211
213
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
212
214
  channel: text("channel").notNull(),
213
- config: text("config").notNull(),
215
+ config: text("config", { mode: "json" }).$type().notNull(),
214
216
  webhookSecret: text("webhook_secret"),
215
- enabled: integer("enabled").notNull().default(1),
217
+ enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
216
218
  createdAt: text("created_at").notNull(),
217
219
  updatedAt: text("updated_at").notNull()
218
220
  }, (table) => [
@@ -224,7 +226,7 @@ var googleConnections = sqliteTable("google_connections", {
224
226
  connectionType: text("connection_type").notNull(),
225
227
  propertyId: text("property_id"),
226
228
  sitemapUrl: text("sitemap_url"),
227
- scopes: text("scopes").notNull().default("[]"),
229
+ scopes: text("scopes", { mode: "json" }).$type().notNull().default([]),
228
230
  createdAt: text("created_at").notNull(),
229
231
  updatedAt: text("updated_at").notNull()
230
232
  }, (table) => [
@@ -261,9 +263,9 @@ var gscUrlInspections = sqliteTable("gsc_url_inspections", {
261
263
  robotsTxtState: text("robots_txt_state"),
262
264
  crawlTime: text("crawl_time"),
263
265
  lastCrawlResult: text("last_crawl_result"),
264
- isMobileFriendly: integer("is_mobile_friendly"),
265
- richResults: text("rich_results").notNull().default("[]"),
266
- referringUrls: text("referring_urls").notNull().default("[]"),
266
+ isMobileFriendly: integer("is_mobile_friendly", { mode: "boolean" }),
267
+ richResults: text("rich_results", { mode: "json" }).$type().notNull().default([]),
268
+ referringUrls: text("referring_urls", { mode: "json" }).$type().notNull().default([]),
267
269
  inspectedAt: text("inspected_at").notNull(),
268
270
  createdAt: text("created_at").notNull()
269
271
  }, (table) => [
@@ -278,7 +280,7 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
278
280
  date: text("date").notNull(),
279
281
  indexed: integer("indexed").notNull().default(0),
280
282
  notIndexed: integer("not_indexed").notNull().default(0),
281
- reasonBreakdown: text("reason_breakdown").notNull().default("{}"),
283
+ reasonBreakdown: text("reason_breakdown", { mode: "json" }).$type().notNull().default({}),
282
284
  createdAt: text("created_at").notNull()
283
285
  }, (table) => [
284
286
  index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
@@ -311,7 +313,7 @@ var bingUrlInspections = sqliteTable("bing_url_inspections", {
311
313
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
312
314
  url: text("url").notNull(),
313
315
  httpCode: integer("http_code"),
314
- inIndex: integer("in_index"),
316
+ inIndex: integer("in_index", { mode: "boolean" }),
315
317
  lastCrawledDate: text("last_crawled_date"),
316
318
  inIndexDate: text("in_index_date"),
317
319
  inspectedAt: text("inspected_at").notNull(),
@@ -472,8 +474,8 @@ var insights = sqliteTable("insights", {
472
474
  title: text("title").notNull(),
473
475
  query: text("query").notNull(),
474
476
  provider: text("provider").notNull(),
475
- recommendation: text("recommendation"),
476
- cause: text("cause"),
477
+ recommendation: text("recommendation", { mode: "json" }).$type(),
478
+ cause: text("cause", { mode: "json" }).$type(),
477
479
  dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false),
478
480
  createdAt: text("created_at").notNull()
479
481
  }, (table) => [
@@ -489,7 +491,7 @@ var healthSnapshots = sqliteTable("health_snapshots", {
489
491
  overallCitedRate: text("overall_cited_rate").notNull(),
490
492
  totalPairs: integer("total_pairs").notNull(),
491
493
  citedPairs: integer("cited_pairs").notNull(),
492
- providerBreakdown: text("provider_breakdown").notNull().default("{}"),
494
+ providerBreakdown: text("provider_breakdown", { mode: "json" }).$type().notNull().default({}),
493
495
  createdAt: text("created_at").notNull()
494
496
  }, (table) => [
495
497
  index("idx_health_snapshots_project").on(table.projectId),
@@ -589,9 +591,9 @@ var trafficSources = sqliteTable("traffic_sources", {
589
591
  // observed in the most recent successful sync. Bounded ring buffer used to
590
592
  // dedupe across sync runs at the boundary timestamp where lastSyncedAt
591
593
  // clamping alone leaves a small overlap window.
592
- lastEventIds: text("last_event_ids"),
594
+ lastEventIds: text("last_event_ids", { mode: "json" }).$type(),
593
595
  archivedAt: text("archived_at"),
594
- configJson: text("config_json").notNull().default("{}"),
596
+ configJson: text("config_json", { mode: "json" }).$type().notNull().default({}),
595
597
  createdAt: text("created_at").notNull(),
596
598
  updatedAt: text("updated_at").notNull()
597
599
  }, (table) => [
@@ -667,7 +669,7 @@ var rawEventSamples = sqliteTable("raw_event_samples", {
667
669
  pathNormalized: text("path_normalized").notNull(),
668
670
  status: integer("status"),
669
671
  refererHost: text("referer_host"),
670
- classifierDetailsJson: text("classifier_details_json").notNull().default("{}"),
672
+ classifierDetailsJson: text("classifier_details_json", { mode: "json" }).$type().notNull().default({}),
671
673
  createdAt: text("created_at").notNull()
672
674
  }, (table) => [
673
675
  index("idx_raw_event_samples_project_ts").on(table.projectId, table.ts),
@@ -688,7 +690,7 @@ var discoverySessions = sqliteTable("discovery_sessions", {
688
690
  citedCount: integer("cited_count"),
689
691
  aspirationalCount: integer("aspirational_count"),
690
692
  wastedCount: integer("wasted_count"),
691
- competitorMap: text("competitor_map").notNull().default("[]"),
693
+ competitorMap: text("competitor_map", { mode: "json" }).$type().notNull().default([]),
692
694
  error: text("error"),
693
695
  startedAt: text("started_at"),
694
696
  finishedAt: text("finished_at"),
@@ -704,13 +706,43 @@ var discoveryProbes = sqliteTable("discovery_probes", {
704
706
  query: text("query").notNull(),
705
707
  bucket: text("bucket"),
706
708
  citationState: text("citation_state").notNull(),
707
- citedDomains: text("cited_domains").notNull().default("[]"),
709
+ citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
708
710
  rawResponse: text("raw_response"),
709
711
  createdAt: text("created_at").notNull()
710
712
  }, (table) => [
711
713
  index("idx_discovery_probes_session").on(table.sessionId),
712
714
  index("idx_discovery_probes_project").on(table.projectId)
713
715
  ]);
716
+ var contentTargetDismissals = sqliteTable("content_target_dismissals", {
717
+ id: text("id").primaryKey(),
718
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
719
+ targetRef: text("target_ref").notNull(),
720
+ addressedUrl: text("addressed_url"),
721
+ note: text("note"),
722
+ dismissedAt: text("dismissed_at").notNull()
723
+ }, (table) => [
724
+ uniqueIndex("idx_content_target_dismissals_project_ref").on(table.projectId, table.targetRef),
725
+ index("idx_content_target_dismissals_project").on(table.projectId)
726
+ ]);
727
+ var recommendationExplanations = sqliteTable("recommendation_explanations", {
728
+ id: text("id").primaryKey(),
729
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
730
+ targetRef: text("target_ref").notNull(),
731
+ promptVersion: text("prompt_version").notNull(),
732
+ provider: text("provider").notNull(),
733
+ model: text("model").notNull(),
734
+ responseText: text("response_text").notNull(),
735
+ /** Estimated cost in millicents (1/100 of a cent) for audit; 0 if unknown. */
736
+ costMillicents: integer("cost_millicents").notNull().default(0),
737
+ generatedAt: text("generated_at").notNull()
738
+ }, (table) => [
739
+ uniqueIndex("idx_recommendation_explanations_unique").on(
740
+ table.projectId,
741
+ table.targetRef,
742
+ table.promptVersion
743
+ ),
744
+ index("idx_recommendation_explanations_project").on(table.projectId)
745
+ ]);
714
746
  var migrationsTable = sqliteTable("_migrations", {
715
747
  version: integer("version").primaryKey(),
716
748
  name: text("name").notNull(),
@@ -1942,6 +1974,55 @@ var MIGRATION_VERSIONS = [
1942
1974
  `CREATE INDEX IF NOT EXISTS idx_audit_log_project ON audit_log(project_id)`,
1943
1975
  `CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`
1944
1976
  ]
1977
+ },
1978
+ {
1979
+ version: 61,
1980
+ name: "content-target-dismissals",
1981
+ // Persistent per-recommendation dismissal so users can mark a content
1982
+ // opportunity "addressed" after they ship the page. The orchestrator
1983
+ // recomputes opportunities on every report load from live GSC / GA
1984
+ // inventory; without persistent dismissal, a recommendation lingers
1985
+ // until the next sync surfaces the new page (days–weeks of lag).
1986
+ //
1987
+ // Keyed by `(project_id, target_ref)` where `target_ref` is the stable
1988
+ // hash that `computeTargetRef()` already produces — same value the
1989
+ // ContentTargetRowDto exposes, so the client passes back the ref it
1990
+ // sees.
1991
+ statements: [
1992
+ `CREATE TABLE IF NOT EXISTS content_target_dismissals (
1993
+ id TEXT PRIMARY KEY,
1994
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1995
+ target_ref TEXT NOT NULL,
1996
+ addressed_url TEXT,
1997
+ note TEXT,
1998
+ dismissed_at TEXT NOT NULL
1999
+ )`,
2000
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_content_target_dismissals_project_ref ON content_target_dismissals(project_id, target_ref)`,
2001
+ `CREATE INDEX IF NOT EXISTS idx_content_target_dismissals_project ON content_target_dismissals(project_id)`
2002
+ ]
2003
+ },
2004
+ {
2005
+ version: 62,
2006
+ name: "recommendation-explanations",
2007
+ // LLM-generated rationale for content recommendations. Cached per
2008
+ // (project, target_ref, prompt_version) so repeat clicks are free.
2009
+ // Bumping the prompt version invalidates the cache forward without
2010
+ // touching the table.
2011
+ statements: [
2012
+ `CREATE TABLE IF NOT EXISTS recommendation_explanations (
2013
+ id TEXT PRIMARY KEY,
2014
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2015
+ target_ref TEXT NOT NULL,
2016
+ prompt_version TEXT NOT NULL,
2017
+ provider TEXT NOT NULL,
2018
+ model TEXT NOT NULL,
2019
+ response_text TEXT NOT NULL,
2020
+ cost_millicents INTEGER NOT NULL DEFAULT 0,
2021
+ generated_at TEXT NOT NULL
2022
+ )`,
2023
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_recommendation_explanations_unique ON recommendation_explanations(project_id, target_ref, prompt_version)`,
2024
+ `CREATE INDEX IF NOT EXISTS idx_recommendation_explanations_project ON recommendation_explanations(project_id)`
2025
+ ]
1945
2026
  }
1946
2027
  ];
1947
2028
  function isDuplicateColumnError(err) {
@@ -2636,9 +2717,15 @@ function isBlogShaped(path) {
2636
2717
  // ../intelligence/src/content-classifier.ts
2637
2718
  var SEO_STRONG_THRESHOLD = 10;
2638
2719
  var SEO_WEAK_THRESHOLD = 30;
2720
+ function isHomepageOnly(url) {
2721
+ if (url === "/" || url === "") return true;
2722
+ const stripped = url.split("?")[0].replace(/\/+$/, "");
2723
+ return stripped === "" || stripped === "/";
2724
+ }
2639
2725
  function classifyContentAction(input) {
2640
2726
  const { ourPage, ourPageInGroundingSources, ourPageHasSchema } = input;
2641
2727
  if (!ourPage) return "create";
2728
+ if (isHomepageOnly(ourPage.url)) return "create";
2642
2729
  if (ourPageInGroundingSources) {
2643
2730
  if (ourPageHasSchema === false) return "add-schema";
2644
2731
  return null;
@@ -2783,8 +2870,7 @@ function buildContentTargetRows(input) {
2783
2870
  const targetRef = computeTargetRef({
2784
2871
  projectId: input.projectId,
2785
2872
  query: cq.query,
2786
- action,
2787
- targetPage: ourPage?.url ?? null
2873
+ action
2788
2874
  });
2789
2875
  const winningCompetitor = pickTopCompetitor(cq.competitorGroundingUrls);
2790
2876
  const ourBestPage = ourPage ? {
@@ -2936,7 +3022,7 @@ function pickTopCompetitor(competitors2) {
2936
3022
  };
2937
3023
  }
2938
3024
  function computeTargetRef(input) {
2939
- const key = [input.projectId, input.query, input.action, input.targetPage ?? ""].join("|");
3025
+ const key = [input.projectId, input.query, input.action].join("|");
2940
3026
  let hash = 0;
2941
3027
  for (let i = 0; i < key.length; i++) {
2942
3028
  hash = (hash << 5) - hash + key.charCodeAt(i) | 0;
@@ -4136,8 +4222,8 @@ var IntelligenceService = class {
4136
4222
  title: insight.title,
4137
4223
  query: insight.query,
4138
4224
  provider: insight.provider,
4139
- recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
4140
- cause: insight.cause ? JSON.stringify(insight.cause) : null,
4225
+ recommendation: insight.recommendation ?? null,
4226
+ cause: insight.cause ?? null,
4141
4227
  dismissed: wasDismissed,
4142
4228
  createdAt: insight.createdAt
4143
4229
  }).run();
@@ -4149,7 +4235,7 @@ var IntelligenceService = class {
4149
4235
  overallCitedRate: String(result.health.overallCitedRate),
4150
4236
  totalPairs: result.health.totalPairs,
4151
4237
  citedPairs: result.health.citedPairs,
4152
- providerBreakdown: JSON.stringify(result.health.providerBreakdown),
4238
+ providerBreakdown: result.health.providerBreakdown,
4153
4239
  createdAt: now
4154
4240
  }).run();
4155
4241
  });
@@ -4184,7 +4270,7 @@ var IntelligenceService = class {
4184
4270
  const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq(projects.id, projectId)).get();
4185
4271
  const locationCount = Math.max(
4186
4272
  1,
4187
- parseJsonColumn(projectRow?.locations ?? null, []).length
4273
+ (projectRow?.locations ?? []).length
4188
4274
  );
4189
4275
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
4190
4276
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
@@ -4260,8 +4346,8 @@ var IntelligenceService = class {
4260
4346
  orphanCount++;
4261
4347
  continue;
4262
4348
  }
4263
- const domains = parseJsonColumn(r.citedDomains, []);
4264
- const competitors2 = parseJsonColumn(r.competitorOverlap, []);
4349
+ const domains = r.citedDomains;
4350
+ const competitors2 = r.competitorOverlap;
4265
4351
  snapshots.push({
4266
4352
  query: resolvedQuery,
4267
4353
  provider: r.provider,
@@ -4321,6 +4407,8 @@ export {
4321
4407
  rawEventSamples,
4322
4408
  discoverySessions,
4323
4409
  discoveryProbes,
4410
+ contentTargetDismissals,
4411
+ recommendationExplanations,
4324
4412
  createClient,
4325
4413
  parseJsonColumn,
4326
4414
  extractLegacyCredentials,