@agent-native/core-corpus 0.210.2 → 0.211.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
- package/corpus/templates/analytics/CHANGELOG.md +18 -0
- package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
- package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
- package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
- package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
- package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
- package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
- package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
- package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
- package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
- package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
- package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
- package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
- package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
- package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
- package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
- package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
- package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
- package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
- package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
- package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
- package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
- package/corpus/templates/analytics/server/plugins/db.ts +9 -1
- package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
- package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/actions/delete-event.ts +1 -3
- package/corpus/templates/calendar/actions/delete-events.ts +1 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
- package/corpus/templates/calendar/actions/update-event.ts +2 -6
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/CHANGELOG.md +11 -24
- package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
- package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
- package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
- package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
- package/corpus/templates/clips/app/root.tsx +22 -10
- package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
- package/corpus/templates/clips/app/routes/import.tsx +115 -110
- package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +29 -186
- package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
- package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
- package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
- package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
- package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/content/CHANGELOG.md +12 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
- package/corpus/templates/content/actions/_database-navigation.ts +3 -3
- package/corpus/templates/content/actions/_database-utils.ts +10 -22
- package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
- package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
- package/corpus/templates/content/actions/export-document.ts +2 -14
- package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
- package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
- package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
- package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
- package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
- package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
- package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
- package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
- package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
- package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
- package/corpus/templates/content/app/global.css +23 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
- package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
- package/corpus/templates/content/e2e/helpers.ts +10 -0
- package/corpus/templates/content/shared/api.ts +18 -12
- package/corpus/templates/content/shared/database-query.ts +107 -13
- package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/delete-file.ts +1 -4
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
- package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +185 -85
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
- package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
- package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
- package/corpus/templates/mail/AGENTS.md +1 -1
- package/corpus/templates/mail/CHANGELOG.md +14 -0
- package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
- package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
- package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
- package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
- package/corpus/templates/mail/app/root.tsx +10 -2
- package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +9 -0
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/plan/CHANGELOG.md +4 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
- package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
- package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Content",
|
|
3
|
+
"description": "Blog articles with authors, daily GA4 pageviews, Search Console clicks and impressions, and first-touch signups, from Analytics' daily content collection. Reads only Analytics' own tables, so opening it calls no provider.",
|
|
4
|
+
"filters": [
|
|
5
|
+
{
|
|
6
|
+
"id": "timeRange",
|
|
7
|
+
"type": "select",
|
|
8
|
+
"label": "Time range",
|
|
9
|
+
"default": "28d",
|
|
10
|
+
"options": [
|
|
11
|
+
{
|
|
12
|
+
"value": "7d",
|
|
13
|
+
"label": "Last 7 days"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"value": "28d",
|
|
17
|
+
"label": "Last 28 days"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"value": "90d",
|
|
21
|
+
"label": "Last 90 days"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"value": "all",
|
|
25
|
+
"label": "All time"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"panels": [
|
|
31
|
+
{
|
|
32
|
+
"id": "collection-status",
|
|
33
|
+
"title": "Collection",
|
|
34
|
+
"chartType": "table",
|
|
35
|
+
"source": "first-party",
|
|
36
|
+
"width": 3,
|
|
37
|
+
"sql": "WITH latest AS ( SELECT step, status, started_at, window_end, error_message FROM ( SELECT step, status, started_at, window_end, error_message, ROW_NUMBER() OVER (PARTITION BY step ORDER BY started_at DESC) AS rn FROM analytics_content_collection_runs ) ranked WHERE rn = 1 ) SELECT CASE step WHEN 'builder_blog' THEN 'Blog articles' WHEN 'ga4' THEN 'GA4 pageviews' WHEN 'search_console' THEN 'Search Console' WHEN 'signups' THEN 'Signups' ELSE step END AS source, CASE status WHEN 'succeeded' THEN 'Collected' WHEN 'failed' THEN 'Failed' WHEN 'cost_limited' THEN 'Cost-limited' WHEN 'not_configured' THEN 'Not set up' WHEN 'running' THEN 'Running' ELSE status END AS status, started_at AS last_run, (SELECT window_end FROM analytics_content_collection_runs successful WHERE successful.step = latest.step AND successful.status = 'succeeded' ORDER BY successful.started_at DESC LIMIT 1) AS data_through, error_message AS detail FROM latest ORDER BY CASE step WHEN 'builder_blog' THEN 1 WHEN 'ga4' THEN 2 WHEN 'search_console' THEN 3 WHEN 'signups' THEN 4 ELSE 5 END",
|
|
38
|
+
"config": {
|
|
39
|
+
"timeScope": "all-time",
|
|
40
|
+
"description": "Latest run across all time for each source. Data through is the last successful coverage boundary.",
|
|
41
|
+
"columns": [
|
|
42
|
+
{
|
|
43
|
+
"key": "source",
|
|
44
|
+
"label": "Source"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"key": "status",
|
|
48
|
+
"label": "Status"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"key": "last_run",
|
|
52
|
+
"label": "Last run",
|
|
53
|
+
"format": "date"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"key": "data_through",
|
|
57
|
+
"label": "Data through"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"key": "detail",
|
|
61
|
+
"label": "Detail"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "articles",
|
|
68
|
+
"title": "Articles",
|
|
69
|
+
"chartType": "table",
|
|
70
|
+
"source": "first-party",
|
|
71
|
+
"width": 3,
|
|
72
|
+
"sql": "WITH latest AS ( SELECT step, status, started_at, window_end, error_message FROM ( SELECT step, status, started_at, window_end, error_message, ROW_NUMBER() OVER (PARTITION BY step ORDER BY started_at DESC) AS rn FROM analytics_content_collection_runs ) ranked WHERE rn = 1 ), totals AS ( SELECT item_id, SUM(value) FILTER (WHERE metric = 'ga4_pageviews') AS pageviews, COUNT(*) FILTER (WHERE metric = 'ga4_pageviews') AS pageview_rows, SUM(value) FILTER (WHERE metric = 'search_console_clicks') AS clicks, SUM(value) FILTER (WHERE metric = 'search_console_impressions') AS impressions, COUNT(*) FILTER (WHERE metric = 'search_console_clicks') AS search_rows, SUM(value) FILTER (WHERE metric = 'signups_first_touch') AS signups, SUM(value) FILTER (WHERE metric = 'signups_first_touch_source') AS source_signups, COUNT(*) FILTER (WHERE metric = 'signups_first_touch') AS signup_rows FROM analytics_content_metric_snapshots WHERE (metric_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND metric_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '28d' AND metric_date >= to_char(CURRENT_DATE - INTERVAL '28 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND metric_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND metric_date >= '{{timeRangeStart}}' AND metric_date <= '{{timeRangeEnd}}'))) GROUP BY item_id ) SELECT title, href, author, published, pageviews, clicks, impressions, signups, source_signups, NULLIF(COALESCE(ga4_missing, '') || CASE WHEN ga4_missing IS NOT NULL AND search_missing IS NOT NULL THEN ' · ' ELSE '' END || COALESCE(search_missing, '') || CASE WHEN signups_missing IS NOT NULL AND (ga4_missing IS NOT NULL OR search_missing IS NOT NULL) THEN ' · ' ELSE '' END || COALESCE(signups_missing, ''), '') AS not_collected FROM ( SELECT i.title, i.url AS href, i.author, i.published_at, substr(i.published_at, 1, 10) AS published, t.pageviews, t.clicks, t.impressions, t.signups, t.source_signups, CASE WHEN COALESCE(t.pageview_rows, 0) > 0 THEN NULL ELSE 'GA4: ' || CASE ga4_run.status WHEN 'failed' THEN 'failed' WHEN 'cost_limited' THEN 'cost-limited' WHEN 'not_configured' THEN 'not set up' WHEN 'running' THEN 'running' WHEN 'succeeded' THEN 'not reported in this window' ELSE 'not collected yet' END END AS ga4_missing, CASE WHEN COALESCE(t.search_rows, 0) > 0 THEN NULL ELSE 'Search Console: ' || CASE search_run.status WHEN 'failed' THEN 'failed' WHEN 'cost_limited' THEN 'cost-limited' WHEN 'not_configured' THEN 'not set up' WHEN 'running' THEN 'running' WHEN 'succeeded' THEN 'not reported in this window' ELSE 'not collected yet' END END AS search_missing, CASE WHEN COALESCE(t.signup_rows, 0) > 0 THEN NULL ELSE 'Signups: ' || CASE signups_run.status WHEN 'failed' THEN 'failed' WHEN 'cost_limited' THEN 'cost-limited' WHEN 'not_configured' THEN 'not set up' WHEN 'running' THEN 'running' WHEN 'succeeded' THEN 'not reported in this window' ELSE 'not collected yet' END END AS signups_missing FROM analytics_content_items i LEFT JOIN totals t ON t.item_id = i.id LEFT JOIN latest ga4_run ON ga4_run.step = 'ga4' LEFT JOIN latest search_run ON search_run.step = 'search_console' LEFT JOIN latest signups_run ON signups_run.step = 'signups' WHERE i.source = 'builder_blog' ) articles ORDER BY published_at DESC NULLS LAST LIMIT 1000",
|
|
73
|
+
"config": {
|
|
74
|
+
"timeScope": "dashboard",
|
|
75
|
+
"description": "The 1,000 most recently published collected blog articles. A blank number means that source has no data for the article in this window; Not collected says why.",
|
|
76
|
+
"sortable": true,
|
|
77
|
+
"columns": [
|
|
78
|
+
{
|
|
79
|
+
"key": "title",
|
|
80
|
+
"label": "Article",
|
|
81
|
+
"format": "link",
|
|
82
|
+
"linkKey": "href"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"key": "href",
|
|
86
|
+
"hidden": true
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"key": "author",
|
|
90
|
+
"label": "Author"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"key": "published",
|
|
94
|
+
"label": "Published"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"key": "pageviews",
|
|
98
|
+
"label": "Pageviews",
|
|
99
|
+
"format": "number"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"key": "clicks",
|
|
103
|
+
"label": "Search clicks",
|
|
104
|
+
"format": "number"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"key": "impressions",
|
|
108
|
+
"label": "Impressions",
|
|
109
|
+
"format": "number"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"key": "signups",
|
|
113
|
+
"label": "Signups",
|
|
114
|
+
"format": "number"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"key": "source_signups",
|
|
118
|
+
"label": "Source signups",
|
|
119
|
+
"format": "number"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"key": "not_collected",
|
|
123
|
+
"label": "Not collected"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "daily-trend",
|
|
130
|
+
"title": "Daily pageviews and search clicks",
|
|
131
|
+
"chartType": "line",
|
|
132
|
+
"source": "first-party",
|
|
133
|
+
"width": 3,
|
|
134
|
+
"sql": "WITH RECURSIVE dates AS (SELECT CASE '{{timeRange}}' WHEN '7d' THEN CURRENT_DATE - 7 WHEN '28d' THEN CURRENT_DATE - 28 WHEN '90d' THEN CURRENT_DATE - 90 WHEN 'custom' THEN NULLIF('{{timeRangeStart}}', '')::date ELSE (SELECT MIN(metric_date)::date FROM analytics_content_metric_snapshots) END AS day UNION ALL SELECT day + 1 FROM dates WHERE day < CASE WHEN '{{timeRange}}' = 'custom' THEN LEAST(CURRENT_DATE, NULLIF('{{timeRangeEnd}}', '')::date) ELSE CURRENT_DATE END), totals AS (SELECT s.metric_date AS date, SUM(s.value) FILTER (WHERE s.metric = 'ga4_pageviews') AS pageviews, SUM(s.value) FILTER (WHERE s.metric = 'search_console_clicks') AS search_clicks FROM analytics_content_metric_snapshots s JOIN analytics_content_items i ON i.id = s.item_id WHERE i.source = 'builder_blog' AND s.metric IN ('ga4_pageviews', 'search_console_clicks') AND (s.metric_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '28d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '28 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND s.metric_date >= '{{timeRangeStart}}' AND s.metric_date <= '{{timeRangeEnd}}'))) GROUP BY s.metric_date ORDER BY s.metric_date) SELECT to_char(dates.day, 'YYYY-MM-DD') AS date, totals.pageviews, totals.search_clicks FROM dates LEFT JOIN totals ON totals.date = to_char(dates.day, 'YYYY-MM-DD') WHERE dates.day <= CASE WHEN '{{timeRange}}' = 'custom' THEN LEAST(CURRENT_DATE, NULLIF('{{timeRangeEnd}}', '')::date) ELSE CURRENT_DATE END ORDER BY dates.day",
|
|
135
|
+
"config": {
|
|
136
|
+
"timeScope": "dashboard",
|
|
137
|
+
"xKey": "date",
|
|
138
|
+
"yKeys": ["pageviews", "search_clicks"],
|
|
139
|
+
"seriesLabels": {
|
|
140
|
+
"pageviews": "Pageviews",
|
|
141
|
+
"search_clicks": "Search clicks"
|
|
142
|
+
},
|
|
143
|
+
"description": "All blog articles. A day a source hasn't reported is a gap, not a zero."
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "search-pages",
|
|
148
|
+
"title": "Other pages in search",
|
|
149
|
+
"chartType": "table",
|
|
150
|
+
"source": "first-party",
|
|
151
|
+
"width": 3,
|
|
152
|
+
"sql": "SELECT i.url AS page, i.url AS href, SUM(s.value) FILTER (WHERE s.metric = 'search_console_clicks') AS clicks, SUM(s.value) FILTER (WHERE s.metric = 'search_console_impressions') AS impressions FROM analytics_content_items i JOIN analytics_content_metric_snapshots s ON s.item_id = i.id WHERE i.source = 'search_console' AND (s.metric_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '28d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '28 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND s.metric_date >= '{{timeRangeStart}}' AND s.metric_date <= '{{timeRangeEnd}}'))) GROUP BY i.url ORDER BY clicks DESC NULLS LAST, impressions DESC NULLS LAST LIMIT 200",
|
|
153
|
+
"config": {
|
|
154
|
+
"timeScope": "dashboard",
|
|
155
|
+
"description": "Search Console pages that aren't collected blog articles.",
|
|
156
|
+
"sortable": true,
|
|
157
|
+
"columns": [
|
|
158
|
+
{
|
|
159
|
+
"key": "page",
|
|
160
|
+
"label": "Page",
|
|
161
|
+
"format": "link",
|
|
162
|
+
"linkKey": "href"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"key": "href",
|
|
166
|
+
"hidden": true
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"key": "clicks",
|
|
170
|
+
"label": "Search clicks",
|
|
171
|
+
"format": "number"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"key": "impressions",
|
|
175
|
+
"label": "Impressions",
|
|
176
|
+
"format": "number"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
}
|
|
@@ -1136,6 +1136,7 @@ export const analyticsContentCollectionRuns = table(
|
|
|
1136
1136
|
status: text("status").notNull(),
|
|
1137
1137
|
windowStart: text("window_start"),
|
|
1138
1138
|
windowEnd: text("window_end"),
|
|
1139
|
+
configFingerprint: text("config_fingerprint"),
|
|
1139
1140
|
startedAt: text("started_at").notNull(),
|
|
1140
1141
|
finishedAt: text("finished_at"),
|
|
1141
1142
|
rowCount: integer("row_count"),
|
|
@@ -107,7 +107,9 @@ const SummarySchema = z.object({
|
|
|
107
107
|
sourceUpdatedAt: z.string().nullable(),
|
|
108
108
|
status: z.enum(["not_started", "migrating", "migrated", "failed"]),
|
|
109
109
|
targetDashboardId: z.string().nullable(),
|
|
110
|
+
migratedByEmail: z.string().nullable(),
|
|
110
111
|
migratedByCurrentUser: z.boolean(),
|
|
112
|
+
canStartImport: z.boolean(),
|
|
111
113
|
canRetryStaleClaim: z.boolean(),
|
|
112
114
|
failureCode: z.enum(EXTERNAL_DASHBOARD_FAILURE_CODES).nullable(),
|
|
113
115
|
providerStatus: z.number().nullable(),
|
|
@@ -251,20 +253,35 @@ function requireExternalDashboardContext(): ExternalDashboardContext {
|
|
|
251
253
|
return { orgId, userEmail };
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Whether this user may claim the source now; claimExternalDashboard applies
|
|
258
|
+
* the same rule in SQL. Anyone who can open the workbook may start it, retry a
|
|
259
|
+
* failed import, or take over a stuck one. A source that already has a
|
|
260
|
+
* dashboard (a re-import, failed or stuck) stays with its migrator.
|
|
261
|
+
*/
|
|
262
|
+
function canStartImport(
|
|
255
263
|
row: ExternalDashboardRow,
|
|
256
|
-
|
|
257
|
-
):
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
migratedByCurrentUser: boolean,
|
|
265
|
+
): boolean {
|
|
266
|
+
if (row.targetDashboardId !== null && !migratedByCurrentUser) return false;
|
|
267
|
+
if (row.status === "not_started" || row.status === "failed") return true;
|
|
260
268
|
const migrationClaimedAt = row.migrationClaimedAt
|
|
261
269
|
? Date.parse(row.migrationClaimedAt)
|
|
262
270
|
: Number.NaN;
|
|
263
|
-
|
|
271
|
+
return (
|
|
264
272
|
row.status === "migrating" &&
|
|
265
|
-
migratedByCurrentUser &&
|
|
266
273
|
Number.isFinite(migrationClaimedAt) &&
|
|
267
|
-
migrationClaimedAt <= Date.now() - EXTERNAL_DASHBOARD_CLAIM_STALE_AFTER_MS
|
|
274
|
+
migrationClaimedAt <= Date.now() - EXTERNAL_DASHBOARD_CLAIM_STALE_AFTER_MS
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function toSummary(
|
|
279
|
+
row: ExternalDashboardRow,
|
|
280
|
+
userEmail: string,
|
|
281
|
+
): ExternalDashboardSummary {
|
|
282
|
+
const migratedByCurrentUser =
|
|
283
|
+
row.originalMigratorEmail?.trim().toLowerCase() === userEmail;
|
|
284
|
+
const startable = canStartImport(row, migratedByCurrentUser);
|
|
268
285
|
return SummarySchema.parse({
|
|
269
286
|
id: row.id,
|
|
270
287
|
provider: row.provider,
|
|
@@ -272,9 +289,19 @@ function toSummary(
|
|
|
272
289
|
sourceName: row.sourceName,
|
|
273
290
|
sourceUpdatedAt: row.sourceUpdatedAt,
|
|
274
291
|
status: row.status,
|
|
275
|
-
|
|
292
|
+
// Org members see where a migrated source landed so nobody migrates it
|
|
293
|
+
// twice. Opening the dashboard still goes through its own access check.
|
|
294
|
+
targetDashboardId:
|
|
295
|
+
migratedByCurrentUser || row.status === "migrated"
|
|
296
|
+
? row.targetDashboardId
|
|
297
|
+
: null,
|
|
298
|
+
migratedByEmail:
|
|
299
|
+
row.status === "migrated" || row.status === "migrating"
|
|
300
|
+
? (row.originalMigratorEmail?.trim().toLowerCase() ?? null)
|
|
301
|
+
: null,
|
|
276
302
|
migratedByCurrentUser,
|
|
277
|
-
|
|
303
|
+
canStartImport: startable,
|
|
304
|
+
canRetryStaleClaim: row.status === "migrating" && startable,
|
|
278
305
|
failureCode: row.failureCode,
|
|
279
306
|
providerStatus: row.providerStatus,
|
|
280
307
|
elementDiagnostics: JSON.parse(row.elementDiagnostics),
|
|
@@ -392,14 +419,17 @@ export async function claimExternalDashboard(input: {
|
|
|
392
419
|
const claimToken = randomUUID();
|
|
393
420
|
const table = schema.externalDashboardMigrations;
|
|
394
421
|
const sameMigrator = eq(table.originalMigratorEmail, userEmail);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
422
|
+
// The SQL form of canStartImport: a source that already has a dashboard
|
|
423
|
+
// stays with its migrator.
|
|
424
|
+
const openToCaller = or(isNull(table.targetDashboardId), sameMigrator);
|
|
425
|
+
const canStart = and(
|
|
426
|
+
or(eq(table.status, "not_started"), eq(table.status, "failed")),
|
|
427
|
+
openToCaller,
|
|
398
428
|
);
|
|
399
429
|
const canRecoverStaleClaim = and(
|
|
400
430
|
eq(table.status, "migrating"),
|
|
401
431
|
lt(table.migrationClaimedAt, staleBefore),
|
|
402
|
-
|
|
432
|
+
openToCaller,
|
|
403
433
|
);
|
|
404
434
|
const canRemigrate = parsed.remigrate
|
|
405
435
|
? and(eq(table.status, "migrated"), sameMigrator)
|
|
@@ -421,7 +451,6 @@ export async function claimExternalDashboard(input: {
|
|
|
421
451
|
eq(table.orgId, orgId),
|
|
422
452
|
eq(table.provider, parsed.provider),
|
|
423
453
|
eq(table.sourceId, parsed.sourceId),
|
|
424
|
-
or(isNull(table.originalMigratorEmail), sameMigrator),
|
|
425
454
|
or(canStart, canRecoverStaleClaim, canRemigrate),
|
|
426
455
|
),
|
|
427
456
|
)
|
|
@@ -444,16 +473,25 @@ export async function claimExternalDashboard(input: {
|
|
|
444
473
|
.limit(1);
|
|
445
474
|
if (!existing) return { claimed: false, reason: "not_found" };
|
|
446
475
|
|
|
476
|
+
const existingByCaller =
|
|
477
|
+
existing.originalMigratorEmail?.trim().toLowerCase() === userEmail;
|
|
447
478
|
if (
|
|
448
|
-
existing.
|
|
449
|
-
existing.
|
|
479
|
+
existing.status === "failed" &&
|
|
480
|
+
existing.targetDashboardId !== null &&
|
|
481
|
+
!existingByCaller
|
|
450
482
|
) {
|
|
451
|
-
fail("Only the
|
|
483
|
+
fail("Only the person who migrated this workbook can re-import it.", {
|
|
452
484
|
errorCode: "original_migrator_required",
|
|
453
485
|
statusCode: 403,
|
|
454
486
|
});
|
|
455
487
|
}
|
|
456
488
|
if (existing.status === "migrated") {
|
|
489
|
+
if (parsed.remigrate && !existingByCaller) {
|
|
490
|
+
fail("Only the person who migrated this workbook can re-import it.", {
|
|
491
|
+
errorCode: "original_migrator_required",
|
|
492
|
+
statusCode: 403,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
457
495
|
return { claimed: false, reason: "already_migrated" };
|
|
458
496
|
}
|
|
459
497
|
return { claimed: false, reason: "in_progress" };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isActionContractError } from "@agent-native/core/action";
|
|
2
|
+
|
|
3
|
+
import { removeDashboard } from "../dashboards-store.js";
|
|
4
|
+
import {
|
|
5
|
+
failExternalDashboard,
|
|
6
|
+
listExternalDashboards,
|
|
7
|
+
type ExternalDashboardFailureCode,
|
|
8
|
+
} from "./external-dashboards-store.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Record a Sigma import that did not finish, and delete the dashboard it wrote
|
|
12
|
+
* when that dashboard would otherwise be left behind. Only a first import's
|
|
13
|
+
* dashboard is deleted. When the claimed record already had a dashboard (a
|
|
14
|
+
* re-import, a recovery, or a retry of either), this run wrote over the
|
|
15
|
+
* migrator's own dashboard, which the mapping row still tracks.
|
|
16
|
+
*
|
|
17
|
+
* Dashboard ids include the importer's email. So when another person has
|
|
18
|
+
* taken over the claim, this run's dashboard is never theirs and is deleted;
|
|
19
|
+
* when the same person took it over, the id is shared and is kept.
|
|
20
|
+
*/
|
|
21
|
+
export async function abandonSigmaImport(input: {
|
|
22
|
+
workbookId: string;
|
|
23
|
+
claimToken: string;
|
|
24
|
+
failureCode: ExternalDashboardFailureCode;
|
|
25
|
+
dashboardId: string;
|
|
26
|
+
wroteDashboard: boolean;
|
|
27
|
+
/** The claimed record's targetDashboardId, not the request's flags. */
|
|
28
|
+
previousDashboardId: string | null;
|
|
29
|
+
ctx: { email: string; orgId: string };
|
|
30
|
+
}): Promise<void> {
|
|
31
|
+
let claimLost: unknown = null;
|
|
32
|
+
try {
|
|
33
|
+
await failExternalDashboard({
|
|
34
|
+
provider: "sigma",
|
|
35
|
+
sourceId: input.workbookId,
|
|
36
|
+
claimToken: input.claimToken,
|
|
37
|
+
failureCode: input.failureCode,
|
|
38
|
+
preservePreviousMigration: !input.wroteDashboard,
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (
|
|
42
|
+
!isActionContractError(error) ||
|
|
43
|
+
error.errorCode !== "migration_claim_expired"
|
|
44
|
+
) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
claimLost = error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (input.wroteDashboard && input.previousDashboardId === null) {
|
|
51
|
+
let orphaned = true;
|
|
52
|
+
if (claimLost) {
|
|
53
|
+
const [migration] = await listExternalDashboards({
|
|
54
|
+
provider: "sigma",
|
|
55
|
+
sourceIds: [input.workbookId],
|
|
56
|
+
limit: 1,
|
|
57
|
+
});
|
|
58
|
+
orphaned = !migration?.migratedByCurrentUser;
|
|
59
|
+
}
|
|
60
|
+
if (orphaned) await removeDashboard(input.dashboardId, input.ctx);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (claimLost) throw claimLost;
|
|
64
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
withoutInstallTimes,
|
|
20
20
|
type BuiltinDashboardInstall,
|
|
21
21
|
} from "./builtin-dashboards";
|
|
22
|
+
import legacyContentView from "./content-metrics/legacy-content-view.json";
|
|
22
23
|
import { materializeLegacyBuiltinDashboardSettings } from "./legacy-builtin-dashboard-settings";
|
|
23
24
|
import {
|
|
24
25
|
nativeV2DashboardManifests,
|
|
@@ -169,6 +170,7 @@ function candidateFilter() {
|
|
|
169
170
|
or(
|
|
170
171
|
sql`${d.id} LIKE 'demo-node-exporter-%'`,
|
|
171
172
|
sql`${d.id} LIKE 'native-%'`,
|
|
173
|
+
sql`${d.id} LIKE 'content-view-%'`,
|
|
172
174
|
eq(d.id, "risk-meeting"),
|
|
173
175
|
inArray(d.id, [...SEED_BUILTIN_DASHBOARD_IDS]),
|
|
174
176
|
...SEED_BUILTIN_DASHBOARD_IDS.map(
|
|
@@ -309,10 +311,23 @@ export async function runBuiltinDashboardPass(
|
|
|
309
311
|
const built = install ? buildBuiltinDashboard(builtinId, install) : null;
|
|
310
312
|
if (config && !built) reasons.push("unrecognized");
|
|
311
313
|
if (config && built) {
|
|
314
|
+
const shippedConfigs = [built.config];
|
|
315
|
+
if (builtinId === "content-view") {
|
|
316
|
+
shippedConfigs.push({
|
|
317
|
+
...legacyContentView,
|
|
318
|
+
catalog: {
|
|
319
|
+
templateId: "content-view",
|
|
320
|
+
templateVersion: "2026-06-08",
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
312
324
|
const same =
|
|
313
325
|
row.title === built.title &&
|
|
314
|
-
|
|
315
|
-
|
|
326
|
+
shippedConfigs.some(
|
|
327
|
+
(shipped) =>
|
|
328
|
+
stable(withoutInstallTimes(config!)) ===
|
|
329
|
+
stable(withoutInstallTimes(normalizeDashboardConfig(shipped))),
|
|
330
|
+
);
|
|
316
331
|
if (!same) {
|
|
317
332
|
reasons.push(
|
|
318
333
|
shippedVersion(config) !== shippedVersion(built.config)
|
|
@@ -9,6 +9,7 @@ import { contentRunFailureFromError, type ContentStep } from "./step.js";
|
|
|
9
9
|
import { builderBlogStep } from "./steps/builder-blog.js";
|
|
10
10
|
import { ga4Step } from "./steps/ga4.js";
|
|
11
11
|
import { searchConsoleStep } from "./steps/search-console.js";
|
|
12
|
+
import { signupsStep } from "./steps/signups.js";
|
|
12
13
|
import {
|
|
13
14
|
closeStaleContentRuns,
|
|
14
15
|
completeContentRun,
|
|
@@ -24,6 +25,7 @@ const STEPS: Record<ContentCollectionStepId, ContentStep> = {
|
|
|
24
25
|
builder_blog: builderBlogStep,
|
|
25
26
|
ga4: ga4Step,
|
|
26
27
|
search_console: searchConsoleStep,
|
|
28
|
+
signups: signupsStep,
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
export type ContentStepRunSummary = {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
normalizeSearchConsoleProperty,
|
|
5
|
+
searchConsolePropertyMessage,
|
|
6
|
+
searchConsolePropertyProblem,
|
|
7
|
+
} from "../../../shared/search-console-property.js";
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* Collection steps, in the order the daily job runs them. A step's id is the
|
|
5
11
|
* `step` stored on its collection runs and the `source` on what it writes.
|
|
@@ -8,6 +14,7 @@ export const CONTENT_COLLECTION_STEPS = [
|
|
|
8
14
|
"builder_blog",
|
|
9
15
|
"ga4",
|
|
10
16
|
"search_console",
|
|
17
|
+
"signups",
|
|
11
18
|
] as const;
|
|
12
19
|
export type ContentCollectionStepId = (typeof CONTENT_COLLECTION_STEPS)[number];
|
|
13
20
|
|
|
@@ -18,20 +25,100 @@ const stepToggleSchema = z.object({ enabled: z.boolean() });
|
|
|
18
25
|
* property such as `https://www.example.com/`. `pathPrefix` keeps a large
|
|
19
26
|
* site to the pages the Content view tracks.
|
|
20
27
|
*/
|
|
21
|
-
export const
|
|
22
|
-
siteUrl: z.string().trim().min(1),
|
|
28
|
+
export const storedSearchConsoleSiteSchema = z.object({
|
|
29
|
+
siteUrl: z.string().trim().min(1).transform(normalizeSearchConsoleProperty),
|
|
23
30
|
pathPrefix: z.string().trim().startsWith("/").optional(),
|
|
24
31
|
});
|
|
32
|
+
|
|
33
|
+
// Existing configs must stay readable so admins can correct or disable them.
|
|
34
|
+
export const searchConsoleSiteSchema = storedSearchConsoleSiteSchema.extend({
|
|
35
|
+
siteUrl: storedSearchConsoleSiteSchema.shape.siteUrl.superRefine(
|
|
36
|
+
(siteUrl, ctx) => {
|
|
37
|
+
const problem = searchConsolePropertyProblem(siteUrl);
|
|
38
|
+
if (problem) {
|
|
39
|
+
ctx.addIssue({
|
|
40
|
+
code: "custom",
|
|
41
|
+
message: searchConsolePropertyMessage(siteUrl, problem),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
});
|
|
25
47
|
export type SearchConsoleSite = z.infer<typeof searchConsoleSiteSchema>;
|
|
26
48
|
|
|
49
|
+
const bigQueryTableSchema = z
|
|
50
|
+
.string()
|
|
51
|
+
.trim()
|
|
52
|
+
.regex(
|
|
53
|
+
/^[A-Za-z0-9_-]+.[A-Za-z0-9_]+.[A-Za-z0-9_]+$/,
|
|
54
|
+
"Use project.dataset.table",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const signupSourceSchema = z
|
|
58
|
+
.string()
|
|
59
|
+
.trim()
|
|
60
|
+
.regex(/^[a-z0-9_-]+$/);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The per-article signups join reads the warehouse, so it stays off until an
|
|
64
|
+
* admin names its tables and a bytes cap measured by a dry run.
|
|
65
|
+
*/
|
|
66
|
+
export const signupsConfigSchema = z.object({
|
|
67
|
+
enabled: z.boolean(),
|
|
68
|
+
/** Signup events with JSON `data` holding `visitorId`, `userId` and `signupSource`. */
|
|
69
|
+
signupEventsTable: bigQueryTableSchema.nullable().default(null),
|
|
70
|
+
/** One row per visitor's first pageview: `url`, `visitor_id`, `is_bot`, `created_date`. */
|
|
71
|
+
firstPageviewsTable: bigQueryTableSchema.nullable().default(null),
|
|
72
|
+
signupSource: signupSourceSchema.nullable().default(null),
|
|
73
|
+
/** Earliest signup counted, as YYYY-MM-DD; it bounds the events scan. */
|
|
74
|
+
signupsSince: z.iso.date().nullable().default(null),
|
|
75
|
+
maxBytesBilled: z
|
|
76
|
+
.number()
|
|
77
|
+
.int()
|
|
78
|
+
.positive()
|
|
79
|
+
.max(750_000_000_000)
|
|
80
|
+
.nullable()
|
|
81
|
+
.default(null),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A change to the signups settings: only the fields given. No defaults, so
|
|
86
|
+
* merging it over the saved settings never resets one that was left out.
|
|
87
|
+
*/
|
|
88
|
+
export const signupsSettingsUpdateSchema = z.object({
|
|
89
|
+
enabled: z.boolean().optional(),
|
|
90
|
+
signupEventsTable: bigQueryTableSchema.nullable().optional(),
|
|
91
|
+
firstPageviewsTable: bigQueryTableSchema.nullable().optional(),
|
|
92
|
+
signupSource: signupSourceSchema.nullable().optional(),
|
|
93
|
+
signupsSince: z.iso.date().nullable().optional(),
|
|
94
|
+
maxBytesBilled: z
|
|
95
|
+
.number()
|
|
96
|
+
.int()
|
|
97
|
+
.positive()
|
|
98
|
+
.max(750_000_000_000)
|
|
99
|
+
.nullable()
|
|
100
|
+
.optional(),
|
|
101
|
+
});
|
|
102
|
+
export type SignupsConfig = z.infer<typeof signupsConfigSchema>;
|
|
103
|
+
|
|
27
104
|
export const contentCollectionConfigSchema = z.object({
|
|
28
105
|
builderBlog: stepToggleSchema.default({ enabled: true }),
|
|
29
106
|
ga4: stepToggleSchema.default({ enabled: true }),
|
|
30
107
|
// Off until a property owner grants the service account access; a run
|
|
31
108
|
// before then would only record "no access".
|
|
32
109
|
searchConsole: stepToggleSchema
|
|
33
|
-
.extend({
|
|
110
|
+
.extend({
|
|
111
|
+
sites: z.array(storedSearchConsoleSiteSchema).max(20).default([]),
|
|
112
|
+
})
|
|
34
113
|
.default({ enabled: false, sites: [] }),
|
|
114
|
+
signups: signupsConfigSchema.default({
|
|
115
|
+
enabled: false,
|
|
116
|
+
signupEventsTable: null,
|
|
117
|
+
firstPageviewsTable: null,
|
|
118
|
+
signupSource: null,
|
|
119
|
+
signupsSince: null,
|
|
120
|
+
maxBytesBilled: null,
|
|
121
|
+
}),
|
|
35
122
|
});
|
|
36
123
|
|
|
37
124
|
export type ContentCollectionConfig = z.infer<
|
|
@@ -53,6 +140,8 @@ export function stepEnabled(
|
|
|
53
140
|
return config.ga4.enabled;
|
|
54
141
|
case "search_console":
|
|
55
142
|
return config.searchConsole.enabled;
|
|
143
|
+
case "signups":
|
|
144
|
+
return config.signups.enabled;
|
|
56
145
|
}
|
|
57
146
|
}
|
|
58
147
|
|