@agent-native/core-corpus 0.210.1-nightly-20261020093350 → 0.210.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.
Files changed (74) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  3. package/corpus/templates/analytics/CHANGELOG.md +14 -0
  4. package/corpus/templates/analytics/actions/builder-blog-articles.ts +184 -40
  5. package/corpus/templates/analytics/actions/configure-content-collection.ts +74 -0
  6. package/corpus/templates/analytics/actions/get-content-collection.ts +33 -0
  7. package/corpus/templates/analytics/actions/get-first-party-analytics-backend.ts +3 -0
  8. package/corpus/templates/analytics/actions/query-dbt-semantic-metric.ts +15 -12
  9. package/corpus/templates/analytics/actions/run-content-collection.ts +50 -0
  10. package/corpus/templates/analytics/changelog/2026-10-15-analytics-can-collect-daily-ga4-pageviews-and-search-console.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-10-15-analytics-can-collect-your-blog-article-list-once-a-day-into.md +6 -0
  12. package/corpus/templates/analytics/changelog/2026-10-15-the-blog-article-tool-now-returns-each-article-s-author-and-.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-10-20-blog-article-credits-include-direct-and-wrapped-author-refer.md +6 -0
  14. package/corpus/templates/analytics/changelog/2026-10-20-content-collection-recovers-abandoned-runs-avoids-duplicate-.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-10-20-daily-content-metrics-recover-collection-gaps-and-keep-omitt.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-20-fixed-analytics-dbt-metric-lookups-that-returned-a-bad-gatew.md +6 -0
  17. package/corpus/templates/analytics/netlify.toml +3 -0
  18. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +60 -17
  19. package/corpus/templates/analytics/server/db/schema.ts +104 -0
  20. package/corpus/templates/analytics/server/jobs/content-metrics-collection.ts +20 -0
  21. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +141 -0
  22. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +69 -0
  23. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +67 -0
  24. package/corpus/templates/analytics/server/lib/content-metrics/status.ts +96 -0
  25. package/corpus/templates/analytics/server/lib/content-metrics/step.ts +62 -0
  26. package/corpus/templates/analytics/server/lib/content-metrics/steps/builder-blog.ts +67 -0
  27. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +145 -0
  28. package/corpus/templates/analytics/server/lib/content-metrics/steps/ga4.ts +115 -0
  29. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +176 -0
  30. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +502 -0
  31. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +44 -13
  32. package/corpus/templates/analytics/server/lib/google-analytics.ts +121 -9
  33. package/corpus/templates/analytics/server/lib/search-console.ts +105 -0
  34. package/corpus/templates/analytics/server/plugins/content-metrics-collection-jobs.ts +43 -0
  35. package/corpus/templates/analytics/server/plugins/db.ts +20 -8
  36. package/corpus/templates/analytics/server/routes/api/content-metrics/collect.post.ts +19 -0
  37. package/corpus/templates/brain/CHANGELOG.md +6 -0
  38. package/corpus/templates/brain/actions/list-sources.ts +105 -39
  39. package/corpus/templates/brain/changelog/2026-10-19-brain-source-list-uses-fewer-database-queries.md +6 -0
  40. package/corpus/templates/brain/server/lib/audiences.ts +21 -8
  41. package/corpus/templates/brain/server/lib/zoom.ts +56 -24
  42. package/corpus/templates/clips/CHANGELOG.md +1 -0
  43. package/corpus/templates/content/CHANGELOG.md +7 -0
  44. package/corpus/templates/design/CHANGELOG.md +59 -0
  45. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +6 -1
  46. package/corpus/templates/design/app/lib/prompt-image-optimization.ts +1 -1
  47. package/corpus/templates/design/app/pages/Index.tsx +79 -45
  48. package/corpus/templates/design/changelog/2026-10-20-high-resolution-reference-images-are-resized-in-prompts.md +6 -0
  49. package/corpus/templates/design/changelog/2026-10-20-skip-prompt-moves-beside-suggestions.md +6 -0
  50. package/corpus/templates/design/changelog/2026-10-20-the-design-home-chat-keeps-its-model-picker-visible-while-re.md +6 -0
  51. package/corpus/templates/slides/CHANGELOG.md +6 -0
  52. package/corpus/templates/slides/actions/_uploaded-files.ts +128 -13
  53. package/corpus/templates/slides/actions/import-docx.ts +6 -2
  54. package/corpus/templates/slides/actions/import-file.ts +12 -2
  55. package/corpus/templates/slides/actions/import-pptx.ts +6 -2
  56. package/corpus/templates/slides/changelog/2026-10-20-slides-handles-chat-attachments-more-reliably-including-docu.md +6 -0
  57. package/corpus/templates/slides/server/lib/chat-attachments.ts +27 -7
  58. package/package.json +1 -1
  59. package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  60. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  61. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  62. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  63. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  64. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  65. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  66. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  67. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  68. package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +0 -301
  69. package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +0 -6
  70. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  71. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  72. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  73. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
  74. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 13185
35
+ - template files: 13164
@@ -125,6 +125,25 @@ explaining it.
125
125
 
126
126
  Do not use `app-db` as a dashboard source. For first-party events collected through `/track`, use `source: "first-party"` or the `query-agent-native-analytics` action rather than raw internal `db-query`.
127
127
 
128
+ Per-article content metrics also read through `first-party`:
129
+ `analytics_content_items` (one row per article, with author and publish date),
130
+ `analytics_content_metric_snapshots` (one value per item, day and metric), and
131
+ `analytics_content_collection_runs` (one row per daily collection step). A
132
+ daily job fills them, so a panel over them never calls BigQuery or a provider.
133
+ A missing snapshot means "not collected", never zero: show a step's latest run
134
+ status (`failed`, `cost_limited`, `not_configured`) instead of rendering a gap
135
+ as 0. These tables cannot be joined with `analytics_events` in one query. Turn
136
+ collection on with `configure-content-collection`, check it with
137
+ `get-content-collection`, and rerun a step with `run-content-collection`.
138
+
139
+ Metrics are `ga4_pageviews` (per blog article) and `search_console_clicks` /
140
+ `search_console_impressions` (per article, plus non-article pages stored as
141
+ `search_console` items). Search Console data is two to three days late, so the
142
+ newest days have no row yet; read them as still arriving. Search Console
143
+ collection stays off until a property owner adds the service account that
144
+ `get-content-collection` returns as `serviceAccount.email`; then set its
145
+ `searchConsoleSites` and turn it on.
146
+
128
147
  There is no dbt Semantic Layer panel source. For a Semantic Layer metric, answer in chat with `query-dbt-semantic-metric`; build a warehouse panel only on a table the owner names.
129
148
 
130
149
  AI-generated first-party panels are dashboard-time-bound by default. Set
@@ -3,10 +3,21 @@
3
3
  All notable user-facing changes to Agent-Native Analytics are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-20
7
+
8
+ ### Fixed
9
+
10
+ - Blog article credits include direct and wrapped author references.
11
+ - Content collection recovers abandoned runs, avoids duplicate work, and preserves missing author names.
12
+ - Daily content metrics recover collection gaps and keep omitted Search Console values uncollected.
13
+ - Fixed Analytics dbt metric lookups that returned a bad-gateway error when the metric existed; the error now names the failing field.
14
+
6
15
  ## 2026-10-19
7
16
 
8
17
  ### Fixed
9
18
 
19
+ - Composer focus remains safe when an editor changes during navigation.
20
+ - Dashboard API guidance works without an open dashboard
10
21
  - Dashboard upgrades preserve saved history, custom data sources, and legacy edits.
11
22
  - The agent sees the row breakdown open in your current browser tab.
12
23
 
@@ -93,6 +104,8 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
93
104
 
94
105
  ### Added
95
106
 
107
+ - Analytics can collect daily GA4 pageviews and Search Console clicks and impressions for each blog article
108
+ - Analytics can collect your blog article list once a day into its own tables, ready for the Content view
96
109
  - Click any bar, line point, funnel stage or slice on the first-party dashboard to open the underlying records in a pop-up, with filter, SQL and CSV export.
97
110
  - Add key dates and data notes from the dashboard's new Annotations menu or a chart's menu, show or hide key-date sets per dashboard, and mark a note fixed or hide it on one chart from its hover card.
98
111
  - Data notes shade the days a chart's numbers were affected by an incident or a tracking problem, on the dashboard and in scheduled email reports.
@@ -101,6 +114,7 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
101
114
 
102
115
  ### Improved
103
116
 
117
+ - The blog article tool now returns each article's author and co-authors
104
118
  - Collapsed Analytics navigation icons stay centered when the list is short
105
119
  - Active-user answers distinguish Builder product activity from Agent-Native app opens, and dbt metric follow-ups keep their original filters.
106
120
  - Analytics keeps ambiguous active-user questions open across product activity and membership sources, and validates dbt metric date ranges before reporting results
@@ -1,4 +1,4 @@
1
- import { defineAction } from "@agent-native/core/action";
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
2
  import type { ActionRunContext } from "@agent-native/core/action";
3
3
  import { z } from "zod";
4
4
 
@@ -9,6 +9,8 @@ import {
9
9
 
10
10
  const BUILDER_CONTENT_API_URL =
11
11
  "https://cdn.builder.io/api/v3/content/blog-article";
12
+ const BUILDER_AUTHOR_API_URL =
13
+ "https://cdn.builder.io/api/v3/content/blog-author";
12
14
  const BUILDER_CONTENT_FIELDS = [
13
15
  "id",
14
16
  "name",
@@ -19,6 +21,14 @@ const BUILDER_CONTENT_FIELDS = [
19
21
  "data.handle",
20
22
  "data.date",
21
23
  "data.url",
24
+ "data.author",
25
+ "data.authors",
26
+ ].join(",");
27
+ const BUILDER_AUTHOR_FIELDS = [
28
+ "id",
29
+ "name",
30
+ "data.fullName",
31
+ "data.handle",
22
32
  ].join(",");
23
33
  const BUILDER_PAGE_SIZE = 100;
24
34
  const BUILDER_MAX_PAGES = 200;
@@ -35,17 +45,39 @@ type BuilderContentResult = {
35
45
  handle?: unknown;
36
46
  date?: unknown;
37
47
  url?: unknown;
48
+ author?: unknown;
49
+ authors?: unknown;
38
50
  };
39
51
  };
40
52
 
53
+ type BuilderAuthorResult = {
54
+ id?: unknown;
55
+ name?: unknown;
56
+ data?: { fullName?: unknown; handle?: unknown };
57
+ };
58
+
41
59
  type BuilderContentResponse = {
42
60
  results?: unknown;
43
61
  };
44
62
 
63
+ /**
64
+ * A blog-author reference. `name` and `handle` are null when the referenced
65
+ * author entry is unpublished or deleted; `id` still says who it was.
66
+ */
67
+ export interface BuilderBlogAuthor {
68
+ id: string;
69
+ name: string | null;
70
+ handle: string | null;
71
+ }
72
+
45
73
  export interface BuilderBlogArticle {
46
74
  id: string;
47
75
  handle: string;
48
76
  title: string;
77
+ /** The article's primary author, or null when none is set. */
78
+ author: BuilderBlogAuthor | null;
79
+ /** Every credited author, primary first, without duplicates. */
80
+ authors: BuilderBlogAuthor[];
49
81
  publishDate: string | null;
50
82
  firstPublished: string | null;
51
83
  createdDate: string | null;
@@ -59,6 +91,12 @@ export interface BuilderBlogArticleListResult {
59
91
  nextOffset: number | null;
60
92
  }
61
93
 
94
+ type ArticleWithAuthorIds = {
95
+ article: Omit<BuilderBlogArticle, "author" | "authors">;
96
+ primaryAuthorId: string | null;
97
+ authorIds: string[];
98
+ };
99
+
62
100
  function normalizeHandle(value: unknown): string | null {
63
101
  if (typeof value !== "string") return null;
64
102
  const trimmed = value.trim();
@@ -79,23 +117,57 @@ function normalizeDate(value: unknown): string | null {
79
117
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
80
118
  }
81
119
 
120
+ function nonEmptyString(value: unknown): string | null {
121
+ return typeof value === "string" && value.trim() ? value.trim() : null;
122
+ }
123
+
124
+ function referenceId(value: unknown): string | null {
125
+ if (!value || typeof value !== "object") return null;
126
+ return nonEmptyString((value as { id?: unknown }).id);
127
+ }
128
+
129
+ function articleAuthorIds(data: BuilderContentResult["data"]): {
130
+ primary: string | null;
131
+ all: string[];
132
+ } {
133
+ const ids = (value: unknown): string[] => {
134
+ const entries = Array.isArray(value) ? value : [value];
135
+ return entries
136
+ .map(
137
+ (entry) =>
138
+ referenceId(entry) ??
139
+ referenceId((entry as { author?: unknown } | null)?.author),
140
+ )
141
+ .filter((id): id is string => id !== null);
142
+ };
143
+ const primaryIds = ids(data?.author);
144
+ const primary = primaryIds[0] ?? null;
145
+ const all = [...new Set([...primaryIds, ...ids(data?.authors)])];
146
+ return { primary, all };
147
+ }
148
+
82
149
  function normalizeArticle(
83
150
  result: BuilderContentResult,
84
- ): BuilderBlogArticle | null {
151
+ ): ArticleWithAuthorIds | null {
85
152
  const handle = normalizeHandle(
86
153
  result.data?.handle ?? result.handle ?? result.data?.url,
87
154
  );
88
155
  if (!handle) return null;
156
+ const authorIds = articleAuthorIds(result.data);
89
157
  return {
90
- id: typeof result.id === "string" ? result.id : handle,
91
- handle,
92
- title: typeof result.name === "string" ? result.name : handle,
93
- publishDate: normalizeDate(
94
- result.data?.date ?? result.firstPublished ?? result.createdDate,
95
- ),
96
- firstPublished: normalizeDate(result.firstPublished),
97
- createdDate: normalizeDate(result.createdDate),
98
- lastUpdated: normalizeDate(result.lastUpdated),
158
+ primaryAuthorId: authorIds.primary,
159
+ authorIds: authorIds.all,
160
+ article: {
161
+ id: typeof result.id === "string" ? result.id : handle,
162
+ handle,
163
+ title: typeof result.name === "string" ? result.name : handle,
164
+ publishDate: normalizeDate(
165
+ result.data?.date ?? result.firstPublished ?? result.createdDate,
166
+ ),
167
+ firstPublished: normalizeDate(result.firstPublished),
168
+ createdDate: normalizeDate(result.createdDate),
169
+ lastUpdated: normalizeDate(result.lastUpdated),
170
+ },
99
171
  };
100
172
  }
101
173
 
@@ -109,6 +181,75 @@ function requestContext(context?: ActionRunContext) {
109
181
  };
110
182
  }
111
183
 
184
+ async function fetchBuilderResults(
185
+ url: URL,
186
+ label: string,
187
+ signal: AbortSignal | undefined,
188
+ ): Promise<unknown[]> {
189
+ const response = await fetch(url, { ...(signal ? { signal } : {}) });
190
+ if (!response.ok) {
191
+ fail(`${label} returned HTTP ${response.status}.`, {
192
+ errorCode: "provider_error",
193
+ statusCode: 502,
194
+ });
195
+ }
196
+
197
+ let payload: BuilderContentResponse;
198
+ try {
199
+ payload = (await response.json()) as BuilderContentResponse;
200
+ } catch {
201
+ fail(`${label} returned invalid JSON.`, {
202
+ errorCode: "provider_error",
203
+ statusCode: 502,
204
+ });
205
+ }
206
+ if (!Array.isArray(payload.results)) {
207
+ fail(`${label} returned no results array.`, {
208
+ errorCode: "provider_error",
209
+ statusCode: 502,
210
+ });
211
+ }
212
+ return payload.results;
213
+ }
214
+
215
+ async function resolveAuthors(
216
+ ids: string[],
217
+ apiKey: string,
218
+ signal: AbortSignal | undefined,
219
+ ): Promise<Map<string, BuilderBlogAuthor>> {
220
+ const resolved = new Map<string, BuilderBlogAuthor>();
221
+ for (let start = 0; start < ids.length; start += BUILDER_PAGE_SIZE) {
222
+ const url = new URL(BUILDER_AUTHOR_API_URL);
223
+ url.searchParams.set("apiKey", apiKey);
224
+ url.searchParams.set("fields", BUILDER_AUTHOR_FIELDS);
225
+ url.searchParams.set("limit", String(BUILDER_PAGE_SIZE));
226
+ url.searchParams.set(
227
+ "query.id.$in",
228
+ JSON.stringify(ids.slice(start, start + BUILDER_PAGE_SIZE)),
229
+ );
230
+ url.searchParams.set("noTraverse", "true");
231
+
232
+ const results = await fetchBuilderResults(
233
+ url,
234
+ "Builder Content API (blog authors)",
235
+ signal,
236
+ );
237
+ for (const raw of results) {
238
+ if (!raw || typeof raw !== "object") continue;
239
+ const author = raw as BuilderAuthorResult;
240
+ const id = nonEmptyString(author.id);
241
+ if (!id) continue;
242
+ resolved.set(id, {
243
+ id,
244
+ name:
245
+ nonEmptyString(author.data?.fullName) ?? nonEmptyString(author.name),
246
+ handle: nonEmptyString(author.data?.handle),
247
+ });
248
+ }
249
+ }
250
+ return resolved;
251
+ }
252
+
112
253
  export async function listBuilderBlogArticles(
113
254
  args: { handles?: string[]; offset?: number },
114
255
  context?: ActionRunContext,
@@ -119,9 +260,10 @@ export async function listBuilderBlogArticles(
119
260
  ctx: requestContext(context),
120
261
  });
121
262
  if (!credential) {
122
- throw new Error(
123
- "Builder Content API is not connected for this Analytics workspace.",
124
- );
263
+ fail("Builder Content API is not connected for this Analytics workspace.", {
264
+ errorCode: "builder_not_connected",
265
+ statusCode: 424,
266
+ });
125
267
  }
126
268
 
127
269
  const requestedHandles = new Set(
@@ -135,7 +277,7 @@ export async function listBuilderBlogArticles(
135
277
  "Builder Content API offset must be a non-negative integer.",
136
278
  );
137
279
  }
138
- const found = new Map<string, BuilderBlogArticle>();
280
+ const found = new Map<string, ArticleWithAuthorIds>();
139
281
  let nextOffset: number | null = null;
140
282
 
141
283
  for (let page = 0; page < BUILDER_MAX_PAGES; page += 1) {
@@ -149,37 +291,27 @@ export async function listBuilderBlogArticles(
149
291
  url.searchParams.set("query.published", "published");
150
292
  url.searchParams.set("noTraverse", "true");
151
293
 
152
- const response = await fetch(url, {
153
- ...(context?.signal ? { signal: context.signal } : {}),
154
- });
155
- if (!response.ok) {
156
- throw new Error(`Builder Content API returned HTTP ${response.status}.`);
157
- }
158
-
159
- let payload: BuilderContentResponse;
160
- try {
161
- payload = (await response.json()) as BuilderContentResponse;
162
- } catch {
163
- throw new Error("Builder Content API returned invalid JSON.");
164
- }
165
- if (!Array.isArray(payload.results)) {
166
- throw new Error("Builder Content API returned no results array.");
167
- }
294
+ const results = await fetchBuilderResults(
295
+ url,
296
+ "Builder Content API",
297
+ context?.signal,
298
+ );
168
299
 
169
- for (const raw of payload.results) {
300
+ for (const raw of results) {
170
301
  if (!raw || typeof raw !== "object") continue;
171
- const article = normalizeArticle(raw as BuilderContentResult);
172
- if (!article) continue;
173
- if (requestedHandles.size > 0 && !requestedHandles.has(article.handle)) {
302
+ const normalized = normalizeArticle(raw as BuilderContentResult);
303
+ if (!normalized) continue;
304
+ const { handle } = normalized.article;
305
+ if (requestedHandles.size > 0 && !requestedHandles.has(handle)) {
174
306
  continue;
175
307
  }
176
- if (!found.has(article.handle)) found.set(article.handle, article);
308
+ if (!found.has(handle)) found.set(handle, normalized);
177
309
  }
178
310
 
179
311
  if (
180
312
  (requestedHandles.size > 0 && found.size >= requestedHandles.size) ||
181
- payload.results.length < BUILDER_PAGE_SIZE ||
182
- payload.results.length === 0
313
+ results.length < BUILDER_PAGE_SIZE ||
314
+ results.length === 0
183
315
  ) {
184
316
  break;
185
317
  }
@@ -188,8 +320,20 @@ export async function listBuilderBlogArticles(
188
320
  }
189
321
  }
190
322
 
323
+ const authors = await resolveAuthors(
324
+ [...new Set([...found.values()].flatMap((entry) => entry.authorIds))],
325
+ credential.value,
326
+ context?.signal,
327
+ );
328
+ const authorFor = (id: string): BuilderBlogAuthor =>
329
+ authors.get(id) ?? { id, name: null, handle: null };
330
+
191
331
  return {
192
- articles: [...found.values()],
332
+ articles: [...found.values()].map((entry) => ({
333
+ ...entry.article,
334
+ author: entry.primaryAuthorId ? authorFor(entry.primaryAuthorId) : null,
335
+ authors: entry.authorIds.map(authorFor),
336
+ })),
193
337
  total: found.size,
194
338
  truncated: nextOffset !== null,
195
339
  nextOffset,
@@ -198,7 +342,7 @@ export async function listBuilderBlogArticles(
198
342
 
199
343
  export default defineAction({
200
344
  description:
201
- "List published Builder.io blog-article metadata, including each article handle and canonical data.date publish date. This is a read-only source for joining Builder content to Analytics warehouse metrics; it never returns article bodies. A bounded feed returns truncated=true and nextOffset when more pages remain.",
345
+ "List published Builder.io blog-article metadata: each article's handle, canonical data.date publish date, and authors (author is the primary author; authors lists every credited author, with a null name when the author entry is unpublished). This is a read-only source for joining Builder content to Analytics warehouse metrics; it never returns article bodies. A bounded feed returns truncated=true and nextOffset when more pages remain.",
202
346
  schema: z.object({
203
347
  handles: z
204
348
  .array(z.string().trim().min(1))
@@ -0,0 +1,74 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ contentCollectionConfigSchema,
10
+ searchConsoleSiteSchema,
11
+ } from "../server/lib/content-metrics/config.js";
12
+ import { readContentCollectionStatus } from "../server/lib/content-metrics/status.js";
13
+ import {
14
+ readContentCollection,
15
+ writeContentCollection,
16
+ } from "../server/lib/content-metrics/store.js";
17
+ import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections.js";
18
+ import { cliBoolean } from "./schema-helpers.js";
19
+
20
+ export default defineAction({
21
+ mcpAnnotations: {
22
+ readOnlyHint: false,
23
+ destructiveHint: true,
24
+ openWorldHint: false,
25
+ },
26
+ description:
27
+ "Turn the organization's daily Content view collection on or off and choose its steps. Requires an organization owner or admin. The caller becomes the stored owner: the daily job resolves provider connections as them. Omitted step settings keep their saved values.",
28
+ schema: z.object({
29
+ enabled: cliBoolean.describe("Whether the daily collection runs."),
30
+ builderBlog: cliBoolean
31
+ .optional()
32
+ .describe("Collect the Builder blog article list with authors."),
33
+ ga4: cliBoolean
34
+ .optional()
35
+ .describe("Collect daily GA4 pageviews for each blog article."),
36
+ searchConsole: cliBoolean
37
+ .optional()
38
+ .describe(
39
+ "Collect daily Search Console clicks and impressions. Turn on only after a property owner has given the service account access.",
40
+ ),
41
+ searchConsoleSites: z
42
+ .array(searchConsoleSiteSchema)
43
+ .max(20)
44
+ .optional()
45
+ .describe(
46
+ 'Search Console properties to collect, replacing the saved list, e.g. [{"siteUrl":"sc-domain:example.com","pathPrefix":"/blog/"}].',
47
+ ),
48
+ }),
49
+ run: async (
50
+ { enabled, builderBlog, ga4, searchConsole, searchConsoleSites },
51
+ ctx,
52
+ ) => {
53
+ const admin = await requireAnalyticsAdminContext({
54
+ userEmail: getRequestUserEmail() || ctx?.userEmail,
55
+ orgId: getRequestOrgId() || ctx?.orgId || null,
56
+ });
57
+ const scope = { ownerEmail: admin.userEmail, orgId: admin.orgId };
58
+ const current = await readContentCollection(scope);
59
+ const saved = current?.config ?? contentCollectionConfigSchema.parse({});
60
+ const config = contentCollectionConfigSchema.parse({
61
+ ...saved,
62
+ ...(builderBlog === undefined
63
+ ? {}
64
+ : { builderBlog: { enabled: builderBlog } }),
65
+ ...(ga4 === undefined ? {} : { ga4: { enabled: ga4 } }),
66
+ searchConsole: {
67
+ enabled: searchConsole ?? saved.searchConsole.enabled,
68
+ sites: searchConsoleSites ?? saved.searchConsole.sites,
69
+ },
70
+ });
71
+ await writeContentCollection(scope, { enabled, config });
72
+ return readContentCollectionStatus(admin.orgId, admin.userEmail);
73
+ },
74
+ });
@@ -0,0 +1,33 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { readContentCollectionStatus } from "../server/lib/content-metrics/status.js";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "Read the organization's Content view collection: whether the daily job is on, which steps it runs, the stored owner whose connections it uses, and each step's last run (status succeeded, failed, cost_limited, not_configured or running, with its error and window). Reads Postgres only; it never calls a provider or BigQuery.",
13
+ schema: z.object({}),
14
+ readOnly: true,
15
+ http: { method: "GET" },
16
+ run: async (_args, ctx) => {
17
+ const email = getRequestUserEmail() || ctx?.userEmail;
18
+ if (!email) {
19
+ fail("Sign in to read content collection.", {
20
+ errorCode: "authentication_required",
21
+ statusCode: 401,
22
+ });
23
+ }
24
+ const orgId = getRequestOrgId() || ctx?.orgId;
25
+ if (!orgId) {
26
+ fail("An active organization is required to read content collection.", {
27
+ errorCode: "organization_required",
28
+ statusCode: 403,
29
+ });
30
+ }
31
+ return readContentCollectionStatus(orgId, email);
32
+ },
33
+ });
@@ -22,6 +22,9 @@ export default defineAction({
22
22
  analytics_event_daily_rollups: queryBackendSchema,
23
23
  analytics_user_days: queryBackendSchema,
24
24
  session_recordings: queryBackendSchema,
25
+ analytics_content_items: queryBackendSchema,
26
+ analytics_content_metric_snapshots: queryBackendSchema,
27
+ analytics_content_collection_runs: queryBackendSchema,
25
28
  }),
26
29
  }),
27
30
  http: { method: "GET" },
@@ -588,6 +588,13 @@ class DbtSemanticLayerError extends Error {
588
588
  }
589
589
  }
590
590
 
591
+ // Type names only: dbt's values never go into an error message.
592
+ function shapeOf(value: unknown): string {
593
+ if (value === null) return "null";
594
+ if (Array.isArray(value)) return "array";
595
+ return typeof value;
596
+ }
597
+
591
598
  class DbtRequestTimeoutError extends Error {
592
599
  constructor(message: string) {
593
600
  super(message);
@@ -1371,12 +1378,10 @@ export default defineAction({
1371
1378
  page.totalPages < 0 ||
1372
1379
  page.totalPages > 100_000
1373
1380
  ) {
1374
- fail(
1375
- "dbt Semantic Layer returned incomplete metric metadata pagination.",
1376
- {
1377
- errorCode: "dbt_invalid_metadata_page",
1378
- statusCode: 502,
1379
- },
1381
+ throw new DbtSemanticLayerError(
1382
+ "provider_error",
1383
+ `dbt Semantic Layer returned incomplete metric metadata pagination (items ${shapeOf(page.items)}, totalItems ${shapeOf(page.totalItems)}, totalPages ${shapeOf(page.totalPages)}).`,
1384
+ false,
1380
1385
  );
1381
1386
  }
1382
1387
  if (metadataCursor && page.totalItems !== metadataCursor.of) {
@@ -1442,12 +1447,10 @@ export default defineAction({
1442
1447
  !Number.isInteger(totalItems) ||
1443
1448
  totalItems < 0
1444
1449
  ) {
1445
- fail(
1446
- "dbt Semantic Layer returned an invalid semantic manifest count.",
1447
- {
1448
- errorCode: "dbt_invalid_metadata_count",
1449
- statusCode: 502,
1450
- },
1450
+ throw new DbtSemanticLayerError(
1451
+ "provider_error",
1452
+ `dbt Semantic Layer returned an invalid semantic manifest count (totalItems ${shapeOf(record(manifest.metricsPaginated).totalItems)}).`,
1453
+ false,
1451
1454
  );
1452
1455
  }
1453
1456
  if (totalItems === 0) {
@@ -0,0 +1,50 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { collectContentForCollection } from "../server/lib/content-metrics/collect.js";
9
+ import { CONTENT_COLLECTION_STEPS } from "../server/lib/content-metrics/config.js";
10
+ import { readContentCollection } from "../server/lib/content-metrics/store.js";
11
+ import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections.js";
12
+
13
+ export default defineAction({
14
+ mcpAnnotations: {
15
+ readOnlyHint: false,
16
+ destructiveHint: true,
17
+ openWorldHint: true,
18
+ },
19
+ description:
20
+ "Run the organization's Content view collection now instead of waiting for the daily job. Requires an organization owner or admin and a saved collection (configure-content-collection). Each step calls its provider and records a run; a failed step is recorded as failed and writes no data. Returns each step's status.",
21
+ schema: z.object({
22
+ steps: z
23
+ .array(z.enum(CONTENT_COLLECTION_STEPS))
24
+ .min(1)
25
+ .optional()
26
+ .describe("Steps to run. Defaults to every enabled step."),
27
+ }),
28
+ run: async ({ steps }, ctx) => {
29
+ const admin = await requireAnalyticsAdminContext({
30
+ userEmail: getRequestUserEmail() || ctx?.userEmail,
31
+ orgId: getRequestOrgId() || ctx?.orgId || null,
32
+ });
33
+ const collection = await readContentCollection({
34
+ ownerEmail: admin.userEmail,
35
+ orgId: admin.orgId,
36
+ });
37
+ if (!collection) {
38
+ fail(
39
+ "Content collection is not set up for this organization. Turn it on with configure-content-collection first.",
40
+ { errorCode: "content_collection_not_configured", statusCode: 412 },
41
+ );
42
+ }
43
+ const results = await collectContentForCollection(collection, {
44
+ trigger: "manual",
45
+ ...(steps ? { steps } : {}),
46
+ ...(ctx?.signal ? { signal: ctx.signal } : {}),
47
+ });
48
+ return { steps: results };
49
+ },
50
+ });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-15
4
+ ---
5
+
6
+ Analytics can collect daily GA4 pageviews and Search Console clicks and impressions for each blog article
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-15
4
+ ---
5
+
6
+ Analytics can collect your blog article list once a day into its own tables, ready for the Content view
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-15
4
+ ---
5
+
6
+ The blog article tool now returns each article's author and co-authors
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Blog article credits include direct and wrapped author references.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Content collection recovers abandoned runs, avoids duplicate work, and preserves missing author names.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Daily content metrics recover collection gaps and keep omitted Search Console values uncollected.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Fixed Analytics dbt metric lookups that returned a bad-gateway error when the metric existed; the error now names the failing field.