@agent-native/core-corpus 0.210.1-nightly-20261020124228 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/CHANGELOG.md +14 -0
- package/corpus/templates/analytics/actions/builder-blog-articles.ts +184 -40
- package/corpus/templates/analytics/actions/configure-content-collection.ts +74 -0
- package/corpus/templates/analytics/actions/get-content-collection.ts +33 -0
- package/corpus/templates/analytics/actions/get-first-party-analytics-backend.ts +3 -0
- package/corpus/templates/analytics/actions/query-dbt-semantic-metric.ts +15 -12
- package/corpus/templates/analytics/actions/run-content-collection.ts +50 -0
- package/corpus/templates/analytics/changelog/2026-10-15-analytics-can-collect-daily-ga4-pageviews-and-search-console.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-analytics-can-collect-your-blog-article-list-once-a-day-into.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-the-blog-article-tool-now-returns-each-article-s-author-and-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-blog-article-credits-include-direct-and-wrapped-author-refer.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-content-collection-recovers-abandoned-runs-avoids-duplicate-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-daily-content-metrics-recover-collection-gaps-and-keep-omitt.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-fixed-analytics-dbt-metric-lookups-that-returned-a-bad-gatew.md +6 -0
- package/corpus/templates/analytics/netlify.toml +3 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +60 -17
- package/corpus/templates/analytics/server/db/schema.ts +104 -0
- package/corpus/templates/analytics/server/jobs/content-metrics-collection.ts +20 -0
- package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +141 -0
- package/corpus/templates/analytics/server/lib/content-metrics/config.ts +69 -0
- package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +67 -0
- package/corpus/templates/analytics/server/lib/content-metrics/status.ts +96 -0
- package/corpus/templates/analytics/server/lib/content-metrics/step.ts +62 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/builder-blog.ts +67 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +145 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/ga4.ts +115 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +176 -0
- package/corpus/templates/analytics/server/lib/content-metrics/store.ts +502 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +44 -13
- package/corpus/templates/analytics/server/lib/google-analytics.ts +121 -9
- package/corpus/templates/analytics/server/lib/search-console.ts +105 -0
- package/corpus/templates/analytics/server/plugins/content-metrics-collection-jobs.ts +43 -0
- package/corpus/templates/analytics/server/plugins/db.ts +20 -8
- package/corpus/templates/analytics/server/routes/api/content-metrics/collect.post.ts +19 -0
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/brain/actions/list-sources.ts +105 -39
- package/corpus/templates/brain/changelog/2026-10-19-brain-source-list-uses-fewer-database-queries.md +6 -0
- package/corpus/templates/brain/server/lib/audiences.ts +21 -8
- package/corpus/templates/brain/server/lib/zoom.ts +56 -24
- package/corpus/templates/clips/CHANGELOG.md +1 -0
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/design/CHANGELOG.md +59 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +6 -1
- package/corpus/templates/design/app/lib/prompt-image-optimization.ts +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +79 -45
- package/corpus/templates/design/changelog/2026-10-20-high-resolution-reference-images-are-resized-in-prompts.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-skip-prompt-moves-beside-suggestions.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-the-design-home-chat-keeps-its-model-picker-visible-while-re.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +6 -0
- package/corpus/templates/slides/actions/_uploaded-files.ts +128 -13
- package/corpus/templates/slides/actions/import-docx.ts +6 -2
- package/corpus/templates/slides/actions/import-file.ts +12 -2
- package/corpus/templates/slides/actions/import-pptx.ts +6 -2
- package/corpus/templates/slides/changelog/2026-10-20-slides-handles-chat-attachments-more-reliably-including-docu.md +6 -0
- package/corpus/templates/slides/server/lib/chat-attachments.ts +27 -7
- package/package.json +1 -1
- package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +0 -301
- package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +0 -6
- package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
package/corpus/README.md
CHANGED
|
@@ -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
|
-
):
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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,
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
300
|
+
for (const raw of results) {
|
|
170
301
|
if (!raw || typeof raw !== "object") continue;
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
173
|
-
|
|
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(
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
1375
|
-
"
|
|
1376
|
-
{
|
|
1377
|
-
|
|
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
|
-
|
|
1446
|
-
"
|
|
1447
|
-
{
|
|
1448
|
-
|
|
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
|
+
});
|