@agent-native/core 0.146.3 → 0.146.4
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 +13 -10
- package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/analytics/actions/compose-dashboard.ts +53 -6
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +8 -2
- package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +3 -1
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +48 -9
- package/corpus/templates/analytics/actions/update-dashboard.ts +80 -6
- package/corpus/templates/analytics/changelog/2026-08-07-dashboard-creation-and-edits-now-save-faster-with-reliable-s.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +19 -19
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +21 -2
- package/corpus/templates/analytics/server/lib/bigquery.ts +59 -24
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +44 -8
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +33 -69
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +90 -24
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +17 -2
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +2 -0
- package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/assets/netlify.toml +1 -1
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/scripts/migrate-production.ts +24 -0
- package/corpus/templates/assets/server/plugins/db.ts +1 -1
- package/corpus/templates/assets/tsconfig.json +1 -0
- package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/brain/netlify.toml +1 -1
- package/corpus/templates/brain/package.json +2 -0
- package/corpus/templates/brain/scripts/migrate-production.ts +24 -0
- package/corpus/templates/brain/server/plugins/db.ts +1 -1
- package/corpus/templates/brain/tsconfig.json +1 -0
- package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/calendar/netlify.toml +1 -1
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/scripts/migrate-production.ts +24 -0
- package/corpus/templates/calendar/server/plugins/db.ts +1 -1
- package/corpus/templates/calendar/tsconfig.json +1 -0
- package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/chat/netlify.toml +1 -1
- package/corpus/templates/chat/package.json +1 -0
- package/corpus/templates/chat/scripts/migrate-production.ts +31 -0
- package/corpus/templates/chat/tsconfig.json +1 -0
- package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/clips/netlify.toml +1 -1
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/scripts/migrate-production.ts +24 -0
- package/corpus/templates/clips/server/plugins/db.ts +1 -1
- package/corpus/templates/clips/tsconfig.json +1 -0
- package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/content/netlify.toml +1 -1
- package/corpus/templates/content/package.json +2 -0
- package/corpus/templates/content/scripts/migrate-production.ts +28 -0
- package/corpus/templates/content/server/plugins/db.ts +2 -2
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/crm/netlify.toml +1 -1
- package/corpus/templates/crm/package.json +2 -0
- package/corpus/templates/crm/scripts/migrate-production.ts +24 -0
- package/corpus/templates/crm/server/plugins/db.ts +1 -1
- package/corpus/templates/crm/tsconfig.json +1 -0
- package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/design/netlify.toml +1 -1
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/scripts/migrate-production.ts +24 -0
- package/corpus/templates/design/server/plugins/db.ts +1 -1
- package/corpus/templates/design/tsconfig.json +1 -0
- package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/dispatch/netlify.toml +1 -1
- package/corpus/templates/dispatch/package.json +1 -0
- package/corpus/templates/dispatch/scripts/migrate-production.ts +23 -0
- package/corpus/templates/dispatch/tsconfig.json +1 -0
- package/corpus/templates/factory/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/factory/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/factory/netlify.toml +1 -1
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/scripts/migrate-production.ts +31 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -0
- package/corpus/templates/factory/tsconfig.json +1 -0
- package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/forms/netlify.toml +1 -1
- package/corpus/templates/forms/package.json +2 -0
- package/corpus/templates/forms/scripts/migrate-production.ts +24 -0
- package/corpus/templates/forms/server/plugins/db.ts +1 -1
- package/corpus/templates/forms/tsconfig.json +1 -0
- package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/macros/netlify.toml +1 -1
- package/corpus/templates/macros/package.json +1 -0
- package/corpus/templates/macros/scripts/migrate-production.ts +24 -0
- package/corpus/templates/macros/server/plugins/db.ts +1 -1
- package/corpus/templates/macros/tsconfig.json +1 -0
- package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/mail/netlify.toml +1 -1
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/scripts/migrate-production.ts +24 -0
- package/corpus/templates/mail/server/plugins/db.ts +1 -1
- package/corpus/templates/mail/tsconfig.json +1 -0
- package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/plan/netlify.toml +1 -1
- package/corpus/templates/plan/package.json +1 -0
- package/corpus/templates/plan/scripts/migrate-production.ts +24 -0
- package/corpus/templates/plan/server/plugins/00-mcp.ts +3 -2
- package/corpus/templates/plan/server/plugins/db.ts +1 -1
- package/corpus/templates/plan/tsconfig.json +1 -0
- package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/slides/netlify.toml +1 -1
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/scripts/migrate-production.ts +24 -0
- package/corpus/templates/slides/server/plugins/db.ts +1 -1
- package/corpus/templates/slides/tsconfig.json +1 -0
- package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/tasks/netlify.toml +1 -1
- package/corpus/templates/tasks/package.json +1 -0
- package/corpus/templates/tasks/scripts/migrate-production.ts +24 -0
- package/corpus/templates/tasks/server/plugins/db.ts +1 -1
- package/corpus/templates/tasks/tsconfig.json +1 -0
- package/dist/action.d.ts +2 -0
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/thread-data-builder.d.ts +4 -0
- package/dist/agent/thread-data-builder.js +44 -0
- package/dist/automations/service.d.ts +1 -0
- package/dist/automations/service.js +21 -2
- package/dist/cli/create.js +20 -2
- package/dist/client/agent-page/AgentJobsTab.js +3 -1
- package/dist/client/agent-page/AutomationDetailsDialog.js +9 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/migrations.js +7 -13
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/actions/list-automation-runs.d.ts +1 -0
- package/dist/jobs/actions/list-automation-runs.js +9 -2
- package/dist/jobs/actions/list-recurring-jobs.js +3 -1
- package/dist/jobs/actions/manage-recurring-job.js +1 -1
- package/dist/jobs/actions/run-automation-now.js +1 -0
- package/dist/jobs/background-automation-runner.js +2 -0
- package/dist/jobs/frontmatter.d.ts +2 -0
- package/dist/jobs/frontmatter.js +4 -0
- package/dist/jobs/run-history.d.ts +4 -0
- package/dist/jobs/run-history.js +28 -8
- package/dist/jobs/run-now.d.ts +4 -1
- package/dist/jobs/run-now.js +4 -3
- package/dist/jobs/scheduler.js +18 -0
- package/dist/jobs/tools.d.ts +3 -2
- package/dist/jobs/tools.js +26 -12
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +1 -0
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-routes.d.ts +2 -0
- package/dist/server/action-routes.js +1 -0
- package/dist/server/agent-chat-plugin.js +3 -2
- package/dist/server/builder-browser.js +24 -8
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/dist/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/chat/netlify.toml +1 -1
- package/dist/templates/chat/package.json +1 -0
- package/dist/templates/chat/scripts/migrate-production.ts +31 -0
- package/dist/templates/chat/tsconfig.json +1 -0
- package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/default/package.json +1 -0
- package/dist/templates/default/scripts/migrate-production.ts +31 -0
- package/dist/templates/default/tsconfig.json +1 -0
- package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/workspace-root/AGENTS.md +2 -1
- package/dist/triggers/actions/list-automations.js +1 -1
- package/dist/triggers/actions/manage-automation.js +5 -1
- package/dist/triggers/actions.d.ts +1 -1
- package/dist/triggers/actions.js +21 -15
- package/dist/triggers/dispatcher.js +17 -2
- package/dist/triggers/routes.d.ts +1 -0
- package/dist/triggers/routes.js +1 -0
- package/package.json +1 -1
- package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/chat/netlify.toml +1 -1
- package/src/templates/chat/package.json +1 -0
- package/src/templates/chat/scripts/migrate-production.ts +31 -0
- package/src/templates/chat/tsconfig.json +1 -0
- package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/default/package.json +1 -0
- package/src/templates/default/scripts/migrate-production.ts +31 -0
- package/src/templates/default/tsconfig.json +1 -0
- package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/workspace-root/AGENTS.md +2 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { loadDashboardSeed } from "./dashboard-seeds";
|
|
1
2
|
import {
|
|
3
|
+
buildPanel,
|
|
4
|
+
FIRST_PARTY_DASHBOARD_ID,
|
|
5
|
+
FIRST_PARTY_TEMPLATE_SCOPED_METRIC_KEYS,
|
|
6
|
+
firstPartyTemplateFilter,
|
|
2
7
|
LEGACY_SIGNUPS_OVER_TIME_SQL,
|
|
3
8
|
LEGACY_SEED_SIGNUPS_OVER_TIME_SQL,
|
|
4
9
|
SIGNUPS_OVER_TIME_SQL,
|
|
@@ -12,14 +17,18 @@ const LEGACY_NEW_VS_RECURRING_USERS_DESCRIPTION =
|
|
|
12
17
|
const BOUNDED_NEW_VS_RECURRING_USERS_SQL = `WITH first_seen AS (SELECT NULLIF(user_key, '') AS user_key, MIN(event_date) AS first_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY 1), activity AS (SELECT NULLIF(user_key, '') AS user_key, event_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))), daily AS (SELECT a.event_date AS date, CASE WHEN a.event_date = f.first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
|
|
13
18
|
const MARKETING_SITE_TEMPLATE_FILTER =
|
|
14
19
|
"lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'www'";
|
|
20
|
+
const NEW_VS_TEMPLATE_EXPRESSION =
|
|
21
|
+
"COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')";
|
|
22
|
+
const FIRST_PARTY_NEW_VS_TEMPLATE_FILTER = firstPartyTemplateFilter(
|
|
23
|
+
NEW_VS_TEMPLATE_EXPRESSION,
|
|
24
|
+
);
|
|
15
25
|
export const DEPLOYED_NEW_VS_RECURRING_USERS_SQL =
|
|
16
|
-
BOUNDED_NEW_VS_RECURRING_USERS_SQL.
|
|
17
|
-
|
|
18
|
-
` <> 'docs' AND ${MARKETING_SITE_TEMPLATE_FILTER} AND `,
|
|
26
|
+
BOUNDED_NEW_VS_RECURRING_USERS_SQL.split(" <> 'docs' AND ").join(
|
|
27
|
+
` <> 'docs' AND ${FIRST_PARTY_NEW_VS_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND `,
|
|
19
28
|
);
|
|
20
29
|
// Keep cohort classification in one raw-table scan. Separate first-seen and
|
|
21
30
|
// activity scans double the random heap reads on the growing event table.
|
|
22
|
-
const NEW_VS_RECURRING_USERS_SQL = `WITH activity AS (SELECT NULLIF(user_key, '') AS user_key, event_date, MIN(event_date) OVER (PARTITION BY NULLIF(user_key, '')) AS first_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), daily AS (SELECT event_date AS date, CASE WHEN event_date = first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT user_key) AS users FROM activity WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
|
|
31
|
+
const NEW_VS_RECURRING_USERS_SQL = `WITH activity AS (SELECT NULLIF(user_key, '') AS user_key, event_date, MIN(event_date) OVER (PARTITION BY NULLIF(user_key, '')) AS first_date FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), 'unknown')) <> 'docs' AND ${FIRST_PARTY_NEW_VS_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), daily AS (SELECT event_date AS date, CASE WHEN event_date = first_date THEN 'New' ELSE 'Recurring' END AS user_type, COUNT(DISTINCT user_key) AS users FROM activity WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) GROUP BY 1, 2) SELECT date, user_type, users FROM daily ORDER BY date, CASE WHEN user_type = 'Recurring' THEN 0 ELSE 1 END`;
|
|
23
32
|
const NEW_VS_RECURRING_USERS_DESCRIPTION =
|
|
24
33
|
"Daily signed-in visitors split by first active day observed in the previous 365 days (New) vs return visit (Recurring), stacked with Recurring on the bottom and New on top. Docs and marketing-site traffic are excluded.";
|
|
25
34
|
|
|
@@ -46,11 +55,38 @@ const CANONICAL_CUSTOM_PANEL_REPLACEMENTS: readonly ExactFirstPartyPanelReplacem
|
|
|
46
55
|
},
|
|
47
56
|
];
|
|
48
57
|
|
|
58
|
+
const CANONICAL_CATALOG_PANEL_REPLACEMENTS: readonly ExactFirstPartyPanelReplacement[] =
|
|
59
|
+
(() => {
|
|
60
|
+
const seed = loadDashboardSeed(FIRST_PARTY_DASHBOARD_ID);
|
|
61
|
+
if (!seed || !Array.isArray(seed.panels)) return [];
|
|
62
|
+
const scopedMetricKeys = new Set<string>(
|
|
63
|
+
FIRST_PARTY_TEMPLATE_SCOPED_METRIC_KEYS,
|
|
64
|
+
);
|
|
65
|
+
return seed.panels.flatMap((rawPanel) => {
|
|
66
|
+
if (!rawPanel || typeof rawPanel !== "object") return [];
|
|
67
|
+
const panel = rawPanel as Record<string, unknown>;
|
|
68
|
+
const id = typeof panel.id === "string" ? panel.id : "";
|
|
69
|
+
const legacySql = typeof panel.sql === "string" ? panel.sql : "";
|
|
70
|
+
if (!scopedMetricKeys.has(id)) return [];
|
|
71
|
+
const catalogPanel = id ? buildPanel(id) : null;
|
|
72
|
+
if (!catalogPanel || !legacySql || catalogPanel.sql === legacySql) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
id,
|
|
78
|
+
legacySql: [legacySql],
|
|
79
|
+
sql: catalogPanel.sql,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
});
|
|
83
|
+
})();
|
|
84
|
+
|
|
49
85
|
export function repairCanonicalFirstPartyDashboardQueries(
|
|
50
86
|
config: Record<string, unknown>,
|
|
51
87
|
) {
|
|
52
|
-
return repairFirstPartyObservedRetentionPanels(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
88
|
+
return repairFirstPartyObservedRetentionPanels(config, [
|
|
89
|
+
...CANONICAL_CUSTOM_PANEL_REPLACEMENTS,
|
|
90
|
+
...CANONICAL_CATALOG_PANEL_REPLACEMENTS,
|
|
91
|
+
]);
|
|
56
92
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { SqlDashboardConfig } from "../../app/pages/adhoc/sql-dashboard/types";
|
|
2
|
+
import { repairCanonicalFirstPartyDashboardQueries } from "./canonical-first-party-dashboard-repair";
|
|
2
3
|
import { loadDashboardSeed } from "./dashboard-seeds";
|
|
3
4
|
import {
|
|
4
5
|
listDashboardSummaries,
|
|
5
6
|
type DashboardRecord,
|
|
6
7
|
type DashboardSummaryRecord,
|
|
7
8
|
} from "./dashboards-store";
|
|
8
|
-
import {
|
|
9
|
-
FIRST_PARTY_DASHBOARD_ID,
|
|
10
|
-
repairFirstPartyObservedRetentionPanels,
|
|
11
|
-
} from "./first-party-metric-catalog";
|
|
9
|
+
import { FIRST_PARTY_DASHBOARD_ID } from "./first-party-metric-catalog";
|
|
12
10
|
|
|
13
11
|
export type DashboardTemplateCategory =
|
|
14
12
|
| "Acquisition"
|
|
@@ -68,7 +66,7 @@ function seedConfig(id: string): SqlDashboardConfig {
|
|
|
68
66
|
if (id !== FIRST_PARTY_DASHBOARD_ID) {
|
|
69
67
|
return seed as unknown as SqlDashboardConfig;
|
|
70
68
|
}
|
|
71
|
-
return
|
|
69
|
+
return repairCanonicalFirstPartyDashboardQueries(seed)
|
|
72
70
|
.config as unknown as SqlDashboardConfig;
|
|
73
71
|
}
|
|
74
72
|
|
|
@@ -1031,7 +1031,7 @@ function backfillBranchSql(
|
|
|
1031
1031
|
? [cursor.receivedAt, cursor.receivedAt, cursor.id]
|
|
1032
1032
|
: [];
|
|
1033
1033
|
return {
|
|
1034
|
-
sql: `SELECT
|
|
1034
|
+
sql: `SELECT ${FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ")}
|
|
1035
1035
|
FROM analytics_events
|
|
1036
1036
|
WHERE ${predicate}${cursorSql ? ` AND ${cursorSql}` : ""}
|
|
1037
1037
|
ORDER BY received_at ASC, id ASC LIMIT ?`,
|
|
@@ -1039,20 +1039,32 @@ function backfillBranchSql(
|
|
|
1039
1039
|
};
|
|
1040
1040
|
}
|
|
1041
1041
|
|
|
1042
|
-
function
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1042
|
+
function backfillScopedRowsSql(
|
|
1043
|
+
branches: Array<{ predicate: string; args: string[] }>,
|
|
1044
|
+
cursor: FirstPartyAnalyticsBackfillCursor,
|
|
1045
|
+
limit: number,
|
|
1046
|
+
): { sql: string; args: Array<string | number> } {
|
|
1047
|
+
const branchQueries = branches.map((branch) =>
|
|
1048
|
+
backfillBranchSql(branch.predicate, branch.args, cursor),
|
|
1049
|
+
);
|
|
1050
|
+
const columns = FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ");
|
|
1046
1051
|
return {
|
|
1047
|
-
sql: `
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1052
|
+
sql: `WITH ${branchQueries
|
|
1053
|
+
.map(
|
|
1054
|
+
(_branch, index) =>
|
|
1055
|
+
`backfill_branch_${index} AS (${branchQueries[index]!.sql})`,
|
|
1056
|
+
)
|
|
1057
|
+
.join(", ")}
|
|
1058
|
+
SELECT ${columns}
|
|
1059
|
+
FROM (${branchQueries.map((_branch, index) => `SELECT ${columns} FROM backfill_branch_${index}`).join(" UNION ALL ")}) AS selected_events
|
|
1060
|
+
ORDER BY received_at ASC, id ASC LIMIT ?`,
|
|
1061
|
+
args: [
|
|
1062
|
+
...branchQueries.flatMap((branch) => [...branch.args, limit]),
|
|
1063
|
+
limit,
|
|
1064
|
+
],
|
|
1051
1065
|
};
|
|
1052
1066
|
}
|
|
1053
1067
|
|
|
1054
|
-
const MAX_SQLITE_BIND_VARIABLES = 900;
|
|
1055
|
-
|
|
1056
1068
|
function backfillRowCursor(
|
|
1057
1069
|
row: Record<string, unknown>,
|
|
1058
1070
|
): FirstPartyAnalyticsBackfillCursor {
|
|
@@ -1069,18 +1081,6 @@ function backfillRowCursor(
|
|
|
1069
1081
|
return { receivedAt, id };
|
|
1070
1082
|
}
|
|
1071
1083
|
|
|
1072
|
-
function compareBackfillRows(
|
|
1073
|
-
left: Record<string, unknown>,
|
|
1074
|
-
right: Record<string, unknown>,
|
|
1075
|
-
): number {
|
|
1076
|
-
const leftCursor = backfillRowCursor(left);
|
|
1077
|
-
const rightCursor = backfillRowCursor(right);
|
|
1078
|
-
return (
|
|
1079
|
-
leftCursor.receivedAt.localeCompare(rightCursor.receivedAt) ||
|
|
1080
|
-
leftCursor.id.localeCompare(rightCursor.id)
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
1084
|
export async function backfillFirstPartyAnalyticsBatch(
|
|
1085
1085
|
scope: FirstPartyAnalyticsScope,
|
|
1086
1086
|
cursor: string | null,
|
|
@@ -1108,22 +1108,15 @@ export async function backfillFirstPartyAnalyticsBatch(
|
|
|
1108
1108
|
},
|
|
1109
1109
|
];
|
|
1110
1110
|
const rows: Record<string, unknown>[] = [];
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
timeoutMs: 20_000,
|
|
1121
|
-
maxAttempts: 1,
|
|
1122
|
-
});
|
|
1123
|
-
rows.push(...(result.rows as Record<string, unknown>[]));
|
|
1124
|
-
}
|
|
1125
|
-
rows.sort(compareBackfillRows);
|
|
1126
|
-
const selectedRows = rows.slice(0, boundedLimit);
|
|
1111
|
+
const scoped = backfillScopedRowsSql(branches, parsedCursor, boundedLimit);
|
|
1112
|
+
const result = await db.execute({
|
|
1113
|
+
sql: scoped.sql,
|
|
1114
|
+
args: scoped.args,
|
|
1115
|
+
timeoutMs: 20_000,
|
|
1116
|
+
maxAttempts: 1,
|
|
1117
|
+
});
|
|
1118
|
+
rows.push(...(result.rows as Record<string, unknown>[]));
|
|
1119
|
+
const selectedRows = rows;
|
|
1127
1120
|
if (!selectedRows.length) {
|
|
1128
1121
|
return {
|
|
1129
1122
|
nextCursor: serializeBackfillCursor(parsedCursor),
|
|
@@ -1132,36 +1125,7 @@ export async function backfillFirstPartyAnalyticsBatch(
|
|
|
1132
1125
|
};
|
|
1133
1126
|
}
|
|
1134
1127
|
|
|
1135
|
-
const
|
|
1136
|
-
const hydratedRows: Record<string, unknown>[] = [];
|
|
1137
|
-
for (
|
|
1138
|
-
let offset = 0;
|
|
1139
|
-
offset < selectedIds.length;
|
|
1140
|
-
offset += MAX_SQLITE_BIND_VARIABLES
|
|
1141
|
-
) {
|
|
1142
|
-
const hydratedQuery = backfillRowsByIdsSql(
|
|
1143
|
-
selectedIds.slice(offset, offset + MAX_SQLITE_BIND_VARIABLES),
|
|
1144
|
-
);
|
|
1145
|
-
const hydratedResult = await db.execute({
|
|
1146
|
-
sql: hydratedQuery.sql,
|
|
1147
|
-
args: hydratedQuery.args,
|
|
1148
|
-
timeoutMs: 20_000,
|
|
1149
|
-
maxAttempts: 1,
|
|
1150
|
-
});
|
|
1151
|
-
hydratedRows.push(...(hydratedResult.rows as Record<string, unknown>[]));
|
|
1152
|
-
}
|
|
1153
|
-
const hydratedById = new Map(
|
|
1154
|
-
hydratedRows.map((row) => [backfillRowCursor(row).id, row]),
|
|
1155
|
-
);
|
|
1156
|
-
const selectedEvents = selectedIds.map((id) => {
|
|
1157
|
-
const row = hydratedById.get(id);
|
|
1158
|
-
if (!row) {
|
|
1159
|
-
throw new Error(
|
|
1160
|
-
`First-party analytics backfill row ${id} disappeared before hydration`,
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
return row;
|
|
1164
|
-
});
|
|
1128
|
+
const selectedEvents = selectedRows;
|
|
1165
1129
|
await insertFirstPartyAnalyticsRows(selectedEvents, configuredTable);
|
|
1166
1130
|
const lastCursor = backfillRowCursor(selectedRows[selectedRows.length - 1]!);
|
|
1167
1131
|
return {
|
|
@@ -115,10 +115,64 @@ function fixed(sql: string): (window?: MetricWindow) => string {
|
|
|
115
115
|
|
|
116
116
|
const TEMPLATE_EXPR =
|
|
117
117
|
"COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')";
|
|
118
|
+
/**
|
|
119
|
+
* The first-party catalog is intentionally narrower than every value that can
|
|
120
|
+
* appear in shared browser telemetry. Preview hosts and marketing traffic can
|
|
121
|
+
* use the same event table, but they are not product-template series.
|
|
122
|
+
*/
|
|
123
|
+
export const FIRST_PARTY_TEMPLATE_NAMES = [
|
|
124
|
+
"analytics",
|
|
125
|
+
"assets",
|
|
126
|
+
"brain",
|
|
127
|
+
"calendar",
|
|
128
|
+
"chat",
|
|
129
|
+
"clips",
|
|
130
|
+
"content",
|
|
131
|
+
"design",
|
|
132
|
+
"dispatch",
|
|
133
|
+
"forms",
|
|
134
|
+
"mail",
|
|
135
|
+
"plan",
|
|
136
|
+
"slides",
|
|
137
|
+
] as const;
|
|
138
|
+
export const FIRST_PARTY_TEMPLATE_SCOPED_METRIC_KEYS = [
|
|
139
|
+
"total-signups",
|
|
140
|
+
"signups-over-time",
|
|
141
|
+
"signups-by-template",
|
|
142
|
+
"total-template-clicks",
|
|
143
|
+
"total-demo-clicks",
|
|
144
|
+
"total-cli-copies",
|
|
145
|
+
"template-interest-over-time",
|
|
146
|
+
"clicks-by-template",
|
|
147
|
+
"demo-clicks-over-time",
|
|
148
|
+
"cli-copies-by-template",
|
|
149
|
+
"cli-copies-over-time",
|
|
150
|
+
"pageviews-over-time",
|
|
151
|
+
"sessions-by-app",
|
|
152
|
+
"repeat-users",
|
|
153
|
+
"recurring-users-by-template",
|
|
154
|
+
"recurring-users-by-template-bar",
|
|
155
|
+
"retention-over-time",
|
|
156
|
+
"one-day-retention-by-template",
|
|
157
|
+
"seven-day-retention-by-template",
|
|
158
|
+
"dau-over-time",
|
|
159
|
+
"wau-over-time",
|
|
160
|
+
] as const;
|
|
161
|
+
const FIRST_PARTY_TEMPLATE_SQL_LIST = FIRST_PARTY_TEMPLATE_NAMES.map(
|
|
162
|
+
(name) => `'${name}'`,
|
|
163
|
+
).join(", ");
|
|
164
|
+
export function firstPartyTemplateFilter(
|
|
165
|
+
templateExpression = TEMPLATE_EXPR,
|
|
166
|
+
): string {
|
|
167
|
+
return `lower(${templateExpression}) IN (${FIRST_PARTY_TEMPLATE_SQL_LIST})`;
|
|
168
|
+
}
|
|
169
|
+
const FIRST_PARTY_TEMPLATE_FILTER = firstPartyTemplateFilter();
|
|
118
170
|
const KNOWN_TEMPLATE_FILTER = `${TEMPLATE_EXPR} <> 'unknown'`;
|
|
119
171
|
const PRODUCT_ACTIVITY_TEMPLATE_FILTER = `lower(${TEMPLATE_EXPR}) <> 'docs'`;
|
|
120
172
|
const MARKETING_SITE_TEMPLATE_FILTER = `lower(${TEMPLATE_EXPR}) <> 'www'`;
|
|
121
173
|
const KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER = `${KNOWN_TEMPLATE_FILTER} AND ${PRODUCT_ACTIVITY_TEMPLATE_FILTER}`;
|
|
174
|
+
const FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER = `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`;
|
|
175
|
+
const FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER = `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`;
|
|
122
176
|
const EVENT_DATE_SQL = "event_date";
|
|
123
177
|
const EVENT_DATE_FILTER_SQL = EVENT_DATE_SQL;
|
|
124
178
|
const USER_KEY_SQL = "NULLIF(user_key, '')";
|
|
@@ -221,11 +275,11 @@ export function usesFirstPartyDashboardFilters(sql: string): boolean {
|
|
|
221
275
|
return sql.includes("{{timeRange}}") || sql.includes("{{emailFilter}}");
|
|
222
276
|
}
|
|
223
277
|
|
|
224
|
-
const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
|
|
278
|
+
const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`;
|
|
225
279
|
const LEGACY_SEED_SIGNUPS_TIME_RANGE_FILTER = `('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(7)}) OR ('{{timeRange}}' = '30d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(30)}) OR ('{{timeRange}}' = '90d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(90)}) OR ('{{timeRange}}' = '180d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(180)}) OR ('{{timeRange}}' = '365d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(365)}))`;
|
|
226
280
|
export const LEGACY_SEED_SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${LEGACY_SEED_SIGNUPS_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
|
|
227
281
|
export const LEGACY_SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
|
|
228
|
-
export const SIGNUPS_OVER_TIME_SQL = `WITH digits AS (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9), offsets AS (SELECT ones.n + tens.n * 10 + hundreds.n * 100 AS n FROM digits ones CROSS JOIN digits tens CROSS JOIN digits hundreds WHERE hundreds.n < 8), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
|
|
282
|
+
export const SIGNUPS_OVER_TIME_SQL = `WITH digits AS (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9), offsets AS (SELECT ones.n + tens.n * 10 + hundreds.n * 100 AS n FROM digits ones CROSS JOIN digits tens CROSS JOIN digits hundreds WHERE hundreds.n < 8), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
|
|
229
283
|
export const LEGACY_RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= ${rollingWindowStartSql()} AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= ${RETENTION_MIN_COHORT_SIZE} ORDER BY cs.date, p.period`;
|
|
230
284
|
export const LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
|
|
231
285
|
export const LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
|
|
@@ -259,7 +313,7 @@ export const LEGACY_V0_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
|
|
|
259
313
|
"CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))",
|
|
260
314
|
"CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))",
|
|
261
315
|
);
|
|
262
|
-
const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
|
|
316
|
+
const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
|
|
263
317
|
export const LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
|
|
264
318
|
export const LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
|
|
265
319
|
const OBSERVED_ACTIVITY_LOOKBACK_FILTER = `${EVENT_DATE_SQL} >= ${daysAgoSql(OBSERVED_ACTIVITY_LOOKBACK_DAYS)}`;
|
|
@@ -291,7 +345,7 @@ export const PRE_MARKETING_SITE_RETENTION_OVER_TIME_SQL =
|
|
|
291
345
|
const RETENTION_OVER_TIME_SQL =
|
|
292
346
|
PRE_MARKETING_SITE_RETENTION_OVER_TIME_SQL.replace(
|
|
293
347
|
PRODUCT_ACTIVITY_TEMPLATE_FILTER,
|
|
294
|
-
`${
|
|
348
|
+
`${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
|
|
295
349
|
);
|
|
296
350
|
export const PRE_MARKETING_SITE_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
|
|
297
351
|
LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
|
|
@@ -303,7 +357,7 @@ export const MATERIALIZED_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
|
|
|
303
357
|
KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER,
|
|
304
358
|
`${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
|
|
305
359
|
);
|
|
306
|
-
const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${
|
|
360
|
+
const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1, 2, 3), observed AS (SELECT user_key, event_date, FIRST_VALUE(template) OVER (PARTITION BY user_key ORDER BY event_date, template) AS starting_template, MIN(event_date) OVER (PARTITION BY user_key ORDER BY event_date, template) AS cohort_date FROM base), cohorts AS (SELECT user_key, starting_template AS template, cohort_date, MAX(CASE WHEN event_date > cohort_date AND event_date <= to_char(cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') THEN 1 ELSE 0 END) AS retained FROM observed WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")} GROUP BY user_key, starting_template, cohort_date) SELECT template, SUM(retained) AS retained_users, COUNT(*) AS cohort_users, SUM(retained)::float / NULLIF(COUNT(*), 0) AS rate FROM cohorts GROUP BY template HAVING COUNT(*) >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cohort_users DESC, template`;
|
|
307
361
|
export const PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
|
|
308
362
|
LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
|
|
309
363
|
`${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen`,
|
|
@@ -312,7 +366,7 @@ export const PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
|
|
|
312
366
|
const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
|
|
313
367
|
PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
|
|
314
368
|
KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER,
|
|
315
|
-
`${
|
|
369
|
+
`${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
|
|
316
370
|
);
|
|
317
371
|
export const PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
|
|
318
372
|
export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_SQL =
|
|
@@ -320,14 +374,14 @@ export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_SQL =
|
|
|
320
374
|
PRODUCT_ACTIVITY_TEMPLATE_FILTER,
|
|
321
375
|
`${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
|
|
322
376
|
);
|
|
323
|
-
const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${
|
|
377
|
+
const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT event_date AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
|
|
324
378
|
export const PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
|
|
325
379
|
export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
|
|
326
380
|
PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL.replace(
|
|
327
381
|
PRODUCT_ACTIVITY_TEMPLATE_FILTER,
|
|
328
382
|
`${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
|
|
329
383
|
);
|
|
330
|
-
const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${
|
|
384
|
+
const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT to_char(date_trunc('week', event_date::date), 'YYYY-MM-DD') AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
|
|
331
385
|
const LEGACY_RECURRING_USERS_DESCRIPTION =
|
|
332
386
|
"Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
|
|
333
387
|
const LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION =
|
|
@@ -461,10 +515,22 @@ export function repairFirstPartyObservedRetentionPanels(
|
|
|
461
515
|
sql: REPEAT_USERS_SQL,
|
|
462
516
|
},
|
|
463
517
|
],
|
|
464
|
-
...additionalReplacements.map(
|
|
465
|
-
(replacement) => [replacement.id, replacement] as const,
|
|
466
|
-
),
|
|
467
518
|
]);
|
|
519
|
+
for (const replacement of additionalReplacements) {
|
|
520
|
+
const existing = replacements.get(replacement.id);
|
|
521
|
+
replacements.set(
|
|
522
|
+
replacement.id,
|
|
523
|
+
existing
|
|
524
|
+
? {
|
|
525
|
+
...existing,
|
|
526
|
+
...replacement,
|
|
527
|
+
legacySql: Array.from(
|
|
528
|
+
new Set([...existing.legacySql, ...replacement.legacySql]),
|
|
529
|
+
),
|
|
530
|
+
}
|
|
531
|
+
: replacement,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
468
534
|
let changed = false;
|
|
469
535
|
const panels = config.panels.map((rawPanel) => {
|
|
470
536
|
if (!rawPanel || typeof rawPanel !== "object") return rawPanel;
|
|
@@ -509,18 +575,18 @@ export const LEGACY_DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${T
|
|
|
509
575
|
export const PRE_MARKETING_SITE_DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT user_key) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
|
|
510
576
|
const DAU_BY_TEMPLATE_SQL = PRE_MARKETING_SITE_DAU_BY_TEMPLATE_SQL.replace(
|
|
511
577
|
` AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY`,
|
|
512
|
-
` AND ${
|
|
578
|
+
` AND ${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} GROUP BY`,
|
|
513
579
|
);
|
|
514
580
|
export const LEGACY_WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
|
|
515
581
|
export const PRE_MARKETING_SITE_WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT user_key AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
|
|
516
582
|
const WAU_BY_TEMPLATE_SQL = PRE_MARKETING_SITE_WAU_BY_TEMPLATE_SQL.replace(
|
|
517
583
|
` AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}`,
|
|
518
|
-
` AND ${
|
|
584
|
+
` AND ${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}`,
|
|
519
585
|
);
|
|
520
586
|
const PRE_MARKETING_SITE_REPEAT_USERS_SQL = `WITH user_days AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, COUNT(DISTINCT ${EVENT_DATE_SQL}) AS active_days FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) SELECT COUNT(*) AS count FROM user_days WHERE active_days >= 2`;
|
|
521
587
|
const REPEAT_USERS_SQL = PRE_MARKETING_SITE_REPEAT_USERS_SQL.replace(
|
|
522
588
|
PRODUCT_ACTIVITY_TEMPLATE_FILTER,
|
|
523
|
-
`${
|
|
589
|
+
`${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
|
|
524
590
|
);
|
|
525
591
|
|
|
526
592
|
/**
|
|
@@ -590,7 +656,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
590
656
|
width: 2,
|
|
591
657
|
windowed: false,
|
|
592
658
|
buildSql: fixed(
|
|
593
|
-
`SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20`,
|
|
659
|
+
`SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20`,
|
|
594
660
|
),
|
|
595
661
|
config: {
|
|
596
662
|
xKey: "app",
|
|
@@ -698,7 +764,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
698
764
|
width: 1,
|
|
699
765
|
windowed: false,
|
|
700
766
|
buildSql: fixed(
|
|
701
|
-
`SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`,
|
|
767
|
+
`SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`,
|
|
702
768
|
),
|
|
703
769
|
config: {
|
|
704
770
|
yKey: "count",
|
|
@@ -714,7 +780,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
714
780
|
width: 1,
|
|
715
781
|
windowed: false,
|
|
716
782
|
buildSql: fixed(
|
|
717
|
-
`SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`,
|
|
783
|
+
`SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`,
|
|
718
784
|
),
|
|
719
785
|
config: {
|
|
720
786
|
yKey: "count",
|
|
@@ -730,7 +796,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
730
796
|
width: 1,
|
|
731
797
|
windowed: false,
|
|
732
798
|
buildSql: fixed(
|
|
733
|
-
`SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`,
|
|
799
|
+
`SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`,
|
|
734
800
|
),
|
|
735
801
|
config: {
|
|
736
802
|
yKey: "count",
|
|
@@ -746,7 +812,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
746
812
|
width: 2,
|
|
747
813
|
windowed: false,
|
|
748
814
|
buildSql: fixed(
|
|
749
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
815
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
750
816
|
),
|
|
751
817
|
config: {
|
|
752
818
|
xKey: "date",
|
|
@@ -768,7 +834,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
768
834
|
width: 2,
|
|
769
835
|
windowed: false,
|
|
770
836
|
buildSql: fixed(
|
|
771
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
837
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
772
838
|
),
|
|
773
839
|
config: {
|
|
774
840
|
xKey: "date",
|
|
@@ -790,7 +856,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
790
856
|
width: 2,
|
|
791
857
|
windowed: false,
|
|
792
858
|
buildSql: fixed(
|
|
793
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
859
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
794
860
|
),
|
|
795
861
|
config: {
|
|
796
862
|
xKey: "date",
|
|
@@ -812,7 +878,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
812
878
|
width: 2,
|
|
813
879
|
windowed: false,
|
|
814
880
|
buildSql: fixed(
|
|
815
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
881
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
816
882
|
),
|
|
817
883
|
config: {
|
|
818
884
|
xKey: "date",
|
|
@@ -834,7 +900,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
834
900
|
width: 2,
|
|
835
901
|
windowed: false,
|
|
836
902
|
buildSql: fixed(
|
|
837
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
903
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
|
|
838
904
|
),
|
|
839
905
|
config: {
|
|
840
906
|
xKey: "date",
|
|
@@ -860,7 +926,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
860
926
|
// Browser telemetry emits explicit `pageview` events with URL context, so
|
|
861
927
|
// keep pageview panels scoped to that event instead of all tracked events.
|
|
862
928
|
buildSql: fixed(
|
|
863
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template`,
|
|
929
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template`,
|
|
864
930
|
),
|
|
865
931
|
config: {
|
|
866
932
|
xKey: "date",
|
|
@@ -143,7 +143,7 @@ function isDashboardMutationActionName(name: string): boolean {
|
|
|
143
143
|
// dashboard construction, distinct from an analytics-result question. Turns
|
|
144
144
|
// like this may inspect and clone a template without running a metric query.
|
|
145
145
|
const DASHBOARD_CONSTRUCTION_INTENT_TERMS =
|
|
146
|
-
/\b(build|create|make|clone|copy|duplicate|adapt|update|edit|change|modify|rename|adjust|simplify|switch|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
|
|
146
|
+
/\b(build|create|make|clone|copy|duplicate|adapt|update|edit|change|modify|rename|adjust|refresh|simplify|switch|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
|
|
147
147
|
|
|
148
148
|
const DASHBOARD_CONSTRUCTION_TARGET_TERMS =
|
|
149
149
|
/\b(dashboard|extension|panel|widget)\b/i;
|
|
@@ -61,7 +61,20 @@ function hasSuccessfulDashboardSave(
|
|
|
61
61
|
.trim()
|
|
62
62
|
.toLowerCase()
|
|
63
63
|
.replace(/[\s_]+/g, "-");
|
|
64
|
-
|
|
64
|
+
if (!saveActions.has(name)) return false;
|
|
65
|
+
const content = String(result.content ?? "").trim();
|
|
66
|
+
if (!content.startsWith("{")) return true;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(content) as Record<string, unknown>;
|
|
69
|
+
if (parsed.saved === false) return false;
|
|
70
|
+
if (name === "compose-dashboard" && parsed.changed === false) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
// coercion-ok: malformed structured action output fails closed below.
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
65
78
|
});
|
|
66
79
|
}
|
|
67
80
|
|
|
@@ -713,6 +726,7 @@ export function realDataFinalGuard(
|
|
|
713
726
|
}
|
|
714
727
|
if (
|
|
715
728
|
hasDashboardMutationAttempt(context.toolResults) &&
|
|
729
|
+
hasSuccessfulDashboardSave(context.toolResults) &&
|
|
716
730
|
!draftClaimsAnalyticsMetrics(context.text)
|
|
717
731
|
) {
|
|
718
732
|
return null;
|
|
@@ -930,7 +944,8 @@ export default createAgentChatPlugin({
|
|
|
930
944
|
analyticsSourceGuidanceOpening() +
|
|
931
945
|
"DASHBOARD CREATION RULE — You may create dashboard artifacts, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'save this analysis', 'add a chart for...'). Treat a requested saved analysis or deep-dive report as a dashboard request. Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
|
|
932
946
|
"EXECUTION CONTINUITY — An explicit request to build, create, save, or adapt a dashboard or one-off Custom Block authorizes all non-destructive in-app steps required to finish it in the same turn. After querying or scaffolding, continue through extension-data seeding/refresh, dashboard save/embed, and navigation. Do not ask 'want me to proceed?' or stop at an empty shell. Ask one clarification only when metric scope or grain materially changes the result, and pause for destructive changes or external side effects such as sending email or outreach. " +
|
|
933
|
-
"
|
|
947
|
+
"APPROVED MUTATION CONTINUITY — If this turn includes an explicitly approved dashboard action with concrete input, execute that exact action and input immediately. Treat the supplied dashboard id as authoritative: do not reinterpret an existing-dashboard edit as a template clone, ask for a template name, or substitute an inspection step. A dashboard mutation is complete only when the action result proves `saved: true` and the requested change is reflected by `changed: true`, refreshed/changed panel ids, or an equivalent non-empty proof field; a natural-language acknowledgement or a no-op with skipped panels is not success. " +
|
|
948
|
+
"DASHBOARD MUTATION RULE — For first-party dashboard creation or catalog refreshes, use `compose-dashboard` with metric keys; set `refreshExisting: true` when updating matching catalog panels so the server regenerates validated SQL without sending a large SQL payload through the prompt. For ordinary existing edits, use `mutate-dashboard` with structured `operations` in one atomic save; use its short `code` form only for compact layout/config edits. Both forms address panels by id, validate the resulting config, and return proof. Never stream a large multi-panel SQL script or count shifting `/panels/<index>` positions unless the user specifically asks for low-level JSON-pointer operations. " +
|
|
934
949
|
'CUSTOM BLOCK RULE — Analytics can embed sandboxed extensions as dashboard-scoped Custom Blocks, but native panels and Data Programs come first. Do not create one for an ordinary "put X in this dashboard" request. Use `config.extensionId` only for an explicitly requested one-off or bespoke visualization that the native dashboard model cannot represent faithfully. For each new block, set `config.customBlock` with `authoredBy: "agent"`, `intent: "one-off"`, `scope: "dashboard"`, and a categorical `nativeGapReason` of `custom-visualization`, `custom-interaction`, `custom-layout`, or `other`; never store prompt or customer text there. The embed is shared with the dashboard, appears in scheduled reports, and receives dashboard/panel/current-filter context. Use `config.extensionSlotId` only when the user explicitly asks for a personal/per-viewer slot. Slot ids use `analytics.dashboard.<dashboard-id>.panel.<panel-id>` and require `add-extension-slot-target` plus `install-extension`; installs are per-user, so viewers can see different content and report identities may see an empty slot. Use `get-sql-dashboard` panel summaries to inspect an existing Custom Block. ' +
|
|
935
950
|
'EXTENSION DATA-REPAIR RULE — When fixing data in an existing extension-backed dashboard or migrated surface such as Risk Meeting, inspect the current dashboard and extension first, then call `update-extension` with exactly `id`, `operation="edit"`, and a `payloadJson` string containing focused patches/edits that change only the data-loading seam. Never send empty placeholder fields. Preserve the existing layout, CSS, copy, and interactions; never reconstruct the full HTML body for a data-only fix. A request that combines a visual rewrite such as compacting, removing sections, renaming, or changing padding with a data repair is a broad rewrite; after inspecting the current extension, use `operation="replace"` with the complete replacement in `payloadJson`. If a focused edit fails, change the target instead of retrying identical arguments. ' +
|
|
936
951
|
'FIRST-PARTY DASHBOARD TIME RULE — AI-generated `source: "first-party"` panels are dashboard-time-bound by default: set `config.timeScope` to `dashboard` and include a matching dashboard time predicate. `{{timeRange}}` requires a matching `filters` entry with `id: "timeRange"` and `type: "select"`; `{{<id>Start}}`/`{{<id>End}}` require a matching `type: "date-range"` filter with that id. Allowed `timeScope` values are `dashboard`, `fixed-window`, `cohort-history`, and `all-time`; use `all-time` only when the user requests full available history and put all-time, lifetime, or historical in the title or description. Server validation rejects unbound first-party SQL. ' +
|
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
// giving up first for an unrelated reason.
|
|
4
4
|
export const FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS = 45_000;
|
|
5
5
|
export const DASHBOARD_REPORT_ACTION_TIMEOUT_MS = 50_000;
|
|
6
|
+
/** A dashboard save must fail clearly if warehouse SQL validation stalls. */
|
|
7
|
+
export const DASHBOARD_SQL_VALIDATION_TIMEOUT_MS = 10_000;
|