@agent-native/core 0.72.4 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +67 -0
  3. package/corpus/core/docs/content/skills-guide.md +14 -0
  4. package/corpus/core/docs/content/template-clips.md +7 -1
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/durable-background.ts +35 -12
  7. package/corpus/core/src/agent/production-agent.ts +55 -8
  8. package/corpus/core/src/cli/create.ts +1 -1
  9. package/corpus/core/src/cli/skills.ts +450 -19
  10. package/corpus/core/src/client/ConnectBuilderCard.tsx +7 -7
  11. package/corpus/core/src/client/NewWorkspaceAppFlow.tsx +1 -1
  12. package/corpus/core/src/client/blocks/library/AnnotatedCodeBlock.tsx +33 -18
  13. package/corpus/core/src/client/blocks/library/annotation-rail.tsx +3 -4
  14. package/corpus/core/src/client/components/CodeRequiredDialog.tsx +5 -8
  15. package/corpus/core/src/server/agent-chat-plugin.ts +7 -7
  16. package/corpus/core/src/server/auth.ts +7 -0
  17. package/corpus/core/src/server/onboarding-html.ts +95 -9
  18. package/corpus/core/src/server/self-dispatch.ts +8 -1
  19. package/corpus/core/src/templates/default/AGENTS.md +4 -0
  20. package/corpus/core/src/templates/default/package.json +2 -1
  21. package/corpus/core/src/templates/headless/AGENTS.md +4 -0
  22. package/corpus/core/src/templates/headless/package.json +1 -0
  23. package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -0
  24. package/corpus/core/src/templates/workspace-root/README.md +4 -0
  25. package/corpus/core/src/templates/workspace-root/package.json +1 -0
  26. package/corpus/templates/analytics/actions/compose-dashboard.ts +34 -0
  27. package/corpus/templates/analytics/actions/install-dashboard-template.ts +51 -3
  28. package/corpus/templates/analytics/actions/update-dashboard.ts +3 -4
  29. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +175 -48
  30. package/corpus/templates/analytics/app/global.css +15 -13
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +9 -32
  32. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +0 -26
  33. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +0 -23
  34. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +65 -2
  35. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +3 -4
  36. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-charts-now-fill-sparse-template-time-series-suppor.md +6 -0
  37. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-filters-now-apply-consistently-to-first-party-traf.md +6 -0
  38. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-rows-now-auto-fit-panels-so-one-two-or-three-chart.md +6 -0
  39. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-traffic-tables-now-show-top-urls-and-shared-clips-.md +6 -0
  40. package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +5 -0
  41. package/corpus/templates/analytics/changelog/2026-06-24-first-party-template-dashboards-now-include-repeat-users-ret.md +6 -0
  42. package/corpus/templates/analytics/changelog/2026-06-24-retention-charts-now-break-out-1-day-and-7-day-return-rates-.md +6 -0
  43. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +330 -49
  44. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +7 -2
  45. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +35 -8
  46. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +337 -45
  47. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +1 -1
  48. package/corpus/templates/calendar/server/plugins/auth.ts +12 -0
  49. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +7 -7
  50. package/corpus/templates/clips/AGENTS.md +6 -2
  51. package/corpus/templates/clips/app/components/capture-install-options.tsx +20 -4
  52. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -0
  53. package/corpus/templates/clips/app/components/player/player-controls.tsx +62 -32
  54. package/corpus/templates/clips/app/lib/capture-install-options.ts +39 -5
  55. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +786 -323
  56. package/corpus/templates/clips/app/routes/download.tsx +2 -2
  57. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
  58. package/corpus/templates/clips/changelog/2026-06-24-inline-slack-playback-controls-no-longer-shift-when-adjustin.md +6 -0
  59. package/corpus/templates/clips/changelog/2026-06-24-settings-now-puts-what-s-new-in-a-compact-sidebar-prioritize.md +6 -0
  60. package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-console-log-stream-all-le.md +6 -0
  61. package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-fetch-xhr-network-request.md +6 -0
  62. package/corpus/templates/clips/changelog/2026-06-24-the-chrome-extension-option-now-only-appears-on-supported-hosts.md +5 -0
  63. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +13 -8
  64. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  65. package/corpus/templates/clips/chrome-extension/scripts/package.ts +15 -2
  66. package/corpus/templates/clips/chrome-extension/src/overlay.css +4 -25
  67. package/corpus/templates/clips/chrome-extension/src/overlay.ts +1 -3
  68. package/corpus/templates/clips/server/lib/public-agent-context.ts +67 -24
  69. package/corpus/templates/mail/server/plugins/auth.ts +4 -1
  70. package/corpus/templates/plan/app/pages/PlansPage.tsx +271 -197
  71. package/corpus/templates/plan/changelog/2026-06-24-annotated-code-callouts-now-use-numbered-gutter-markers-that.md +6 -0
  72. package/corpus/templates/plan/server/plan-content.ts +45 -2
  73. package/dist/agent/durable-background.d.ts +9 -4
  74. package/dist/agent/durable-background.d.ts.map +1 -1
  75. package/dist/agent/durable-background.js +34 -11
  76. package/dist/agent/durable-background.js.map +1 -1
  77. package/dist/agent/production-agent.d.ts.map +1 -1
  78. package/dist/agent/production-agent.js +52 -8
  79. package/dist/agent/production-agent.js.map +1 -1
  80. package/dist/cli/create.js +1 -1
  81. package/dist/cli/create.js.map +1 -1
  82. package/dist/cli/skills.d.ts.map +1 -1
  83. package/dist/cli/skills.js +361 -19
  84. package/dist/cli/skills.js.map +1 -1
  85. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  86. package/dist/client/ConnectBuilderCard.js +6 -6
  87. package/dist/client/ConnectBuilderCard.js.map +1 -1
  88. package/dist/client/NewWorkspaceAppFlow.js +1 -1
  89. package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
  90. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  91. package/dist/client/blocks/library/AnnotatedCodeBlock.js +14 -10
  92. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  93. package/dist/client/blocks/library/annotation-rail.d.ts +3 -4
  94. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  95. package/dist/client/blocks/library/annotation-rail.js +3 -4
  96. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  97. package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
  98. package/dist/client/components/CodeRequiredDialog.js +2 -1
  99. package/dist/client/components/CodeRequiredDialog.js.map +1 -1
  100. package/dist/server/agent-chat-plugin.js +7 -7
  101. package/dist/server/agent-chat-plugin.js.map +1 -1
  102. package/dist/server/auth.d.ts +7 -0
  103. package/dist/server/auth.d.ts.map +1 -1
  104. package/dist/server/auth.js +1 -0
  105. package/dist/server/auth.js.map +1 -1
  106. package/dist/server/onboarding-html.d.ts +15 -0
  107. package/dist/server/onboarding-html.d.ts.map +1 -1
  108. package/dist/server/onboarding-html.js +73 -9
  109. package/dist/server/onboarding-html.js.map +1 -1
  110. package/dist/server/self-dispatch.d.ts.map +1 -1
  111. package/dist/server/self-dispatch.js +5 -1
  112. package/dist/server/self-dispatch.js.map +1 -1
  113. package/dist/templates/default/AGENTS.md +4 -0
  114. package/dist/templates/default/package.json +2 -1
  115. package/dist/templates/headless/AGENTS.md +4 -0
  116. package/dist/templates/headless/package.json +1 -0
  117. package/dist/templates/workspace-root/AGENTS.md +4 -0
  118. package/dist/templates/workspace-root/README.md +4 -0
  119. package/dist/templates/workspace-root/package.json +1 -0
  120. package/docs/content/skills-guide.md +14 -0
  121. package/docs/content/template-clips.md +7 -1
  122. package/package.json +1 -1
  123. package/src/templates/default/AGENTS.md +4 -0
  124. package/src/templates/default/package.json +2 -1
  125. package/src/templates/headless/AGENTS.md +4 -0
  126. package/src/templates/headless/package.json +1 -0
  127. package/src/templates/workspace-root/AGENTS.md +4 -0
  128. package/src/templates/workspace-root/README.md +4 -0
  129. package/src/templates/workspace-root/package.json +1 -0
@@ -45,6 +45,14 @@ export interface FirstPartyMetric {
45
45
  config: Record<string, unknown>;
46
46
  }
47
47
 
48
+ export interface FirstPartyDashboardFilter {
49
+ id: string;
50
+ type: "select";
51
+ label: string;
52
+ default: string;
53
+ options: Array<{ value: string; label: string }>;
54
+ }
55
+
48
56
  const WINDOW_DAYS: Record<Exclude<MetricWindow, "all">, number> = {
49
57
  "30d": 30,
50
58
  "90d": 90,
@@ -88,6 +96,57 @@ function fixed(sql: string): (window?: MetricWindow) => string {
88
96
  return () => sql;
89
97
  }
90
98
 
99
+ const SIGNUP_TEMPLATE_EXPR =
100
+ "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')";
101
+ const DASHBOARD_TIME_RANGE_FILTER =
102
+ "('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))";
103
+ const DASHBOARD_EMAIL_FILTER =
104
+ "('{{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'))";
105
+ export const FIRST_PARTY_DASHBOARD_FILTERS: FirstPartyDashboardFilter[] = [
106
+ {
107
+ id: "timeRange",
108
+ type: "select",
109
+ label: "Time range",
110
+ default: "90d",
111
+ options: [
112
+ { value: "7d", label: "Last 7 days" },
113
+ { value: "30d", label: "Last 30 days" },
114
+ { value: "90d", label: "Last 90 days" },
115
+ { value: "180d", label: "Last 180 days" },
116
+ { value: "365d", label: "Last 365 days" },
117
+ { value: "all", label: "All time" },
118
+ ],
119
+ },
120
+ {
121
+ id: "emailFilter",
122
+ type: "select",
123
+ label: "Email filter",
124
+ default: "all",
125
+ options: [
126
+ { value: "all", label: "All users" },
127
+ { value: "exclude_builder", label: "Exclude @builder.io" },
128
+ { value: "only_builder", label: "Only @builder.io" },
129
+ ],
130
+ },
131
+ ];
132
+
133
+ export function buildFirstPartyDashboardFilters(): FirstPartyDashboardFilter[] {
134
+ return FIRST_PARTY_DASHBOARD_FILTERS.map((filter) => ({
135
+ ...filter,
136
+ options: filter.options.map((option) => ({ ...option })),
137
+ }));
138
+ }
139
+
140
+ export function usesFirstPartyDashboardFilters(sql: string): boolean {
141
+ return sql.includes("{{timeRange}}") || sql.includes("{{emailFilter}}");
142
+ }
143
+
144
+ const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
145
+ const SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY timestamp) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT substr(timestamp, 1, 10) AS date, ${SIGNUP_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`;
146
+ const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${SIGNUP_TEMPLATE_EXPR} AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, template, MIN(event_date) AS cohort_date FROM base GROUP BY user_key, template), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '1 day', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT c.cohort_date AS date, c.template, COALESCE(COUNT(DISTINCT c.user_key) FILTER (WHERE b.event_date = to_char(c.cohort_date::date + INTERVAL '1 day', 'YYYY-MM-DD'))::float / NULLIF(COUNT(DISTINCT c.user_key), 0), 0) AS rate FROM cohorts c LEFT JOIN base b ON b.user_key = c.user_key AND b.template = c.template GROUP BY c.cohort_date, c.template ORDER BY c.cohort_date, c.template`;
147
+ const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${SIGNUP_TEMPLATE_EXPR} AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, template, MIN(event_date) AS cohort_date FROM base GROUP BY user_key, template), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT c.cohort_date AS date, c.template, COALESCE(COUNT(DISTINCT c.user_key) FILTER (WHERE b.event_date = to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD'))::float / NULLIF(COUNT(DISTINCT c.user_key), 0), 0) AS rate FROM cohorts c LEFT JOIN base b ON b.user_key = c.user_key AND b.template = c.template GROUP BY c.cohort_date, c.template ORDER BY c.cohort_date, c.template`;
148
+ const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${SIGNUP_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 ${SIGNUP_TEMPLATE_EXPR} ORDER BY count DESC`;
149
+
91
150
  /**
92
151
  * Catalog entries. Order here is the default panel order when a caller passes
93
152
  * metrics; callers can reorder by listing keys in the order they want.
@@ -96,19 +155,16 @@ const ENTRIES: FirstPartyMetric[] = [
96
155
  // --- Signups -------------------------------------------------------------
97
156
  {
98
157
  key: "total-signups",
99
- title: "Signups",
158
+ title: "Total Signups",
100
159
  chartType: "metric",
101
160
  source: "first-party",
102
161
  width: 1,
103
162
  windowed: false,
104
- buildSql: fixed(
105
- "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup'",
106
- ),
163
+ buildSql: fixed(TOTAL_SIGNUPS_SQL),
107
164
  config: {
108
- yKey: "count",
165
+ yKey: "signups",
109
166
  yFormatter: "number",
110
- description:
111
- "Better Auth user creation events from all Agent Native templates",
167
+ description: "Total signup events in the selected time range.",
112
168
  },
113
169
  },
114
170
  {
@@ -118,14 +174,18 @@ const ENTRIES: FirstPartyMetric[] = [
118
174
  source: "first-party",
119
175
  width: 2,
120
176
  windowed: false,
121
- buildSql: fixed(
122
- "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
123
- ),
177
+ buildSql: fixed(SIGNUPS_OVER_TIME_SQL),
124
178
  config: {
125
179
  xKey: "date",
126
180
  yKey: "count",
127
- color: "#8b5cf6",
128
- description: "Daily signup events across all Agent Native templates",
181
+ yFormatter: "number",
182
+ pivot: {
183
+ xKey: "date",
184
+ seriesKey: "template",
185
+ valueKey: "count",
186
+ },
187
+ stacked: true,
188
+ description: "Daily signup events stacked by inferred template/app.",
129
189
  },
130
190
  },
131
191
  {
@@ -133,16 +193,15 @@ const ENTRIES: FirstPartyMetric[] = [
133
193
  title: "Signups by Template",
134
194
  chartType: "bar",
135
195
  source: "first-party",
136
- width: 1,
196
+ width: 2,
137
197
  windowed: false,
138
- buildSql: fixed(
139
- "SELECT COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' GROUP BY COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
140
- ),
198
+ buildSql: fixed(SIGNUPS_BY_TEMPLATE_SQL),
141
199
  config: {
142
200
  xKey: "template",
143
201
  yKey: "count",
144
- color: "#8b5cf6",
145
- description: "Signup events grouped by inferred template/app",
202
+ yFormatter: "number",
203
+ color: "var(--brand-purple)",
204
+ description: "Signup events grouped by inferred template/app.",
146
205
  },
147
206
  },
148
207
 
@@ -205,7 +264,7 @@ const ENTRIES: FirstPartyMetric[] = [
205
264
  width: 1,
206
265
  windowed: false,
207
266
  buildSql: fixed(
208
- "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template'",
267
+ `SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`,
209
268
  ),
210
269
  config: {
211
270
  yKey: "count",
@@ -221,7 +280,7 @@ const ENTRIES: FirstPartyMetric[] = [
221
280
  width: 1,
222
281
  windowed: false,
223
282
  buildSql: fixed(
224
- "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo'",
283
+ `SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`,
225
284
  ),
226
285
  config: {
227
286
  yKey: "count",
@@ -237,7 +296,7 @@ const ENTRIES: FirstPartyMetric[] = [
237
296
  width: 1,
238
297
  windowed: false,
239
298
  buildSql: fixed(
240
- "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command'",
299
+ `SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`,
241
300
  ),
242
301
  config: {
243
302
  yKey: "count",
@@ -253,45 +312,85 @@ const ENTRIES: FirstPartyMetric[] = [
253
312
  width: 2,
254
313
  windowed: false,
255
314
  buildSql: fixed(
256
- "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
315
+ `SELECT substr(timestamp, 1, 10) 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 substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
257
316
  ),
258
- config: { xKey: "date", yKey: "count", color: "var(--brand-blue)" },
317
+ config: {
318
+ xKey: "date",
319
+ yKey: "count",
320
+ yFormatter: "number",
321
+ pivot: {
322
+ xKey: "date",
323
+ seriesKey: "template",
324
+ valueKey: "count",
325
+ },
326
+ stacked: true,
327
+ },
259
328
  },
260
329
  {
261
330
  key: "clicks-by-template",
262
- title: "Clicks by Template",
263
- chartType: "bar",
331
+ title: "Clicks by Template Over Time",
332
+ chartType: "area",
264
333
  source: "first-party",
265
- width: 1,
334
+ width: 2,
266
335
  windowed: false,
267
336
  buildSql: fixed(
268
- "SELECT COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' GROUP BY COALESCE(NULLIF(template, ''), 'unknown') ORDER BY count DESC LIMIT 20",
337
+ `SELECT substr(timestamp, 1, 10) 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 substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
269
338
  ),
270
- config: { xKey: "template", yKey: "count", color: "var(--brand-blue)" },
339
+ config: {
340
+ xKey: "date",
341
+ yKey: "count",
342
+ yFormatter: "number",
343
+ pivot: {
344
+ xKey: "date",
345
+ seriesKey: "template",
346
+ valueKey: "count",
347
+ },
348
+ stacked: true,
349
+ },
271
350
  },
272
351
  {
273
352
  key: "demo-clicks-by-template",
274
- title: "Try-Demo Clicks by Template",
275
- chartType: "bar",
353
+ title: "Try-Demo Clicks Over Time",
354
+ chartType: "area",
276
355
  source: "first-party",
277
- width: 1,
356
+ width: 2,
278
357
  windowed: false,
279
358
  buildSql: fixed(
280
- "SELECT COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' GROUP BY COALESCE(NULLIF(template, ''), 'unknown') ORDER BY count DESC LIMIT 20",
359
+ `SELECT substr(timestamp, 1, 10) 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 substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
281
360
  ),
282
- config: { xKey: "template", yKey: "count", color: "var(--brand-teal)" },
361
+ config: {
362
+ xKey: "date",
363
+ yKey: "count",
364
+ yFormatter: "number",
365
+ pivot: {
366
+ xKey: "date",
367
+ seriesKey: "template",
368
+ valueKey: "count",
369
+ },
370
+ stacked: true,
371
+ },
283
372
  },
284
373
  {
285
374
  key: "cli-copies-by-template",
286
- title: "CLI Copies by Template",
287
- chartType: "bar",
375
+ title: "CLI Copies by Template Over Time",
376
+ chartType: "area",
288
377
  source: "first-party",
289
- width: 1,
378
+ width: 2,
290
379
  windowed: false,
291
380
  buildSql: fixed(
292
- "SELECT COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' GROUP BY COALESCE(NULLIF(template, ''), 'unknown') ORDER BY count DESC LIMIT 20",
381
+ `SELECT substr(timestamp, 1, 10) 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 substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
293
382
  ),
294
- config: { xKey: "template", yKey: "count", color: "#06b6d4" },
383
+ config: {
384
+ xKey: "date",
385
+ yKey: "count",
386
+ yFormatter: "number",
387
+ pivot: {
388
+ xKey: "date",
389
+ seriesKey: "template",
390
+ valueKey: "count",
391
+ },
392
+ stacked: true,
393
+ },
295
394
  },
296
395
  {
297
396
  key: "cli-copies-over-time",
@@ -301,9 +400,19 @@ const ENTRIES: FirstPartyMetric[] = [
301
400
  width: 2,
302
401
  windowed: false,
303
402
  buildSql: fixed(
304
- "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
403
+ `SELECT substr(timestamp, 1, 10) 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 substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
305
404
  ),
306
- config: { xKey: "date", yKey: "count", color: "#06b6d4" },
405
+ config: {
406
+ xKey: "date",
407
+ yKey: "count",
408
+ yFormatter: "number",
409
+ pivot: {
410
+ xKey: "date",
411
+ seriesKey: "template",
412
+ valueKey: "count",
413
+ },
414
+ stacked: true,
415
+ },
307
416
  },
308
417
 
309
418
  // --- Activity / pageviews ------------------------------------------------
@@ -314,17 +423,200 @@ const ENTRIES: FirstPartyMetric[] = [
314
423
  source: "first-party",
315
424
  width: 2,
316
425
  windowed: false,
317
- // First-party tracking has no distinct `pageview` event; total tracked
318
- // events per day is the honest activity/pageviews proxy over analytics_events.
426
+ // Browser telemetry emits explicit `pageview` events with URL context, so
427
+ // keep pageview panels scoped to that event instead of all tracked events.
319
428
  buildSql: fixed(
320
- "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events GROUP BY substr(timestamp, 1, 10) ORDER BY date",
429
+ `SELECT substr(timestamp, 1, 10) 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 substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template`,
321
430
  ),
322
431
  config: {
323
432
  xKey: "date",
324
433
  yKey: "count",
325
- color: "var(--brand-blue)",
434
+ yFormatter: "number",
435
+ pivot: {
436
+ xKey: "date",
437
+ seriesKey: "template",
438
+ valueKey: "count",
439
+ },
440
+ stacked: true,
441
+ description: "Pageview events per day stacked by inferred template/app.",
442
+ },
443
+ },
444
+ {
445
+ key: "top-visited-urls",
446
+ title: "Top Visited URLs",
447
+ chartType: "table",
448
+ source: "first-party",
449
+ width: 2,
450
+ windowed: false,
451
+ buildSql: fixed(
452
+ `WITH pageviews AS (SELECT COALESCE(CASE WHEN NULLIF(hostname, '') IS NOT NULL THEN 'https://' || hostname || COALESCE(NULLIF(path, ''), '/') END, NULLIF(url, ''), NULLIF(path, ''), 'unknown') AS url, CASE WHEN NULLIF(hostname, '') IS NOT NULL THEN 'https://' || hostname || COALESCE(NULLIF(path, ''), '/') WHEN lower(COALESCE(NULLIF(url, ''), '')) LIKE 'http://%' OR lower(COALESCE(NULLIF(url, ''), '')) LIKE 'https://%' THEN url WHEN NULLIF(path, '') IS NOT NULL AND substr(path, 1, 1) = '/' AND substr(path, 1, 2) != '//' THEN path ELSE '' END AS href, COUNT(*) AS views, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users, MAX(substr(timestamp, 1, 10)) AS last_seen FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY 1, 2) SELECT url, views, users, last_seen, href FROM pageviews ORDER BY views DESC LIMIT 25`,
453
+ ),
454
+ config: {
455
+ description:
456
+ "Most-viewed URLs in the selected time range. Links open in a new tab.",
457
+ sortable: true,
458
+ limit: 25,
459
+ columns: [
460
+ { key: "url", label: "URL", format: "link", linkKey: "href" },
461
+ { key: "views", label: "Views", format: "number" },
462
+ { key: "users", label: "Users", format: "number" },
463
+ { key: "last_seen", label: "Last seen", format: "date" },
464
+ { key: "href", hidden: true },
465
+ ],
466
+ },
467
+ },
468
+ {
469
+ key: "top-visited-clips",
470
+ title: "Top Visited Clips",
471
+ chartType: "table",
472
+ source: "first-party",
473
+ width: 2,
474
+ windowed: false,
475
+ buildSql: fixed(
476
+ `WITH clip_events AS (SELECT COALESCE(NULLIF(properties::jsonb ->> 'recording_id', ''), NULLIF(path, ''), NULLIF(url, ''), 'unknown') AS clip_key, CASE WHEN lower(COALESCE(NULLIF(url, ''), '')) LIKE 'http://%' OR lower(COALESCE(NULLIF(url, ''), '')) LIKE 'https://%' THEN url WHEN NULLIF(hostname, '') IS NOT NULL THEN 'https://' || hostname || COALESCE(NULLIF(path, ''), '/') WHEN NULLIF(path, '') LIKE '/%' THEN 'https://clips.agent-native.com' || path ELSE '' END AS href, user_id, anonymous_id, timestamp FROM analytics_events WHERE event_name = 'share_view' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND COALESCE(NULLIF(properties::jsonb ->> 'surface', ''), 'clip') = 'clip'), clip_views AS (SELECT clip_key, COALESCE(MAX(NULLIF(href, '')), clip_key) AS href, COUNT(*) AS views, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users, MAX(substr(timestamp, 1, 10)) AS last_seen FROM clip_events GROUP BY clip_key) SELECT CASE WHEN lower(COALESCE(href, '')) LIKE 'http://%' OR lower(COALESCE(href, '')) LIKE 'https://%' THEN href ELSE clip_key END AS clip, views, users, last_seen, href FROM clip_views ORDER BY views DESC LIMIT 25`,
477
+ ),
478
+ config: {
479
+ description: "Most-viewed shared Clips pages in the selected time range.",
480
+ sortable: true,
481
+ limit: 25,
482
+ columns: [
483
+ { key: "clip", label: "Clip URL", format: "link", linkKey: "href" },
484
+ { key: "views", label: "Views", format: "number" },
485
+ { key: "users", label: "Users", format: "number" },
486
+ { key: "last_seen", label: "Last seen", format: "date" },
487
+ { key: "href", hidden: true },
488
+ ],
489
+ },
490
+ },
491
+ {
492
+ key: "repeat-users",
493
+ title: "Repeat Users",
494
+ chartType: "metric",
495
+ source: "first-party",
496
+ width: 1,
497
+ windowed: false,
498
+ buildSql: fixed(
499
+ "WITH user_days AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, COUNT(DISTINCT substr(timestamp, 1, 10)) AS active_days FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) 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')) GROUP BY COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) SELECT COUNT(*) AS count FROM user_days WHERE active_days >= 2",
500
+ ),
501
+ config: {
502
+ yKey: "count",
503
+ yFormatter: "number",
504
+ description:
505
+ "Distinct users with activity on at least two days in the selected time range. Uses user_id first, then anonymous_id.",
506
+ },
507
+ },
508
+ {
509
+ key: "retention-over-time",
510
+ title: "1d / 7d Retention Over Time",
511
+ chartType: "line",
512
+ source: "first-party",
513
+ width: 3,
514
+ windowed: false,
515
+ buildSql: fixed(
516
+ "WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL 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'))), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), cohorts AS (SELECT user_key, cohort_date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))), retention AS (SELECT c.cohort_date AS date, '1d retention' AS period, COALESCE(COUNT(DISTINCT c.user_key) FILTER (WHERE b.event_date = to_char(c.cohort_date::date + INTERVAL '1 day', 'YYYY-MM-DD'))::float / NULLIF(COUNT(DISTINCT c.user_key), 0), 0) AS rate FROM cohorts c LEFT JOIN base b ON b.user_key = c.user_key GROUP BY c.cohort_date UNION ALL SELECT c.cohort_date AS date, '7d retention' AS period, COALESCE(COUNT(DISTINCT c.user_key) FILTER (WHERE b.event_date = to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD'))::float / NULLIF(COUNT(DISTINCT c.user_key), 0), 0) AS rate FROM cohorts c LEFT JOIN base b ON b.user_key = c.user_key GROUP BY c.cohort_date) SELECT date, period, rate FROM retention ORDER BY date, period",
517
+ ),
518
+ config: {
519
+ xKey: "date",
520
+ yKey: "rate",
521
+ yFormatter: "percent",
522
+ pivot: {
523
+ xKey: "date",
524
+ seriesKey: "period",
525
+ valueKey: "rate",
526
+ },
527
+ colors: ["#10b981", "#8b5cf6"],
528
+ description:
529
+ "First-seen daily cohorts in the selected range. Cohorts must be at least seven days old so both lines are mature.",
530
+ },
531
+ },
532
+ {
533
+ key: "one-day-retention-by-template",
534
+ title: "1d Retention by Template",
535
+ chartType: "line",
536
+ source: "first-party",
537
+ width: 2,
538
+ windowed: false,
539
+ buildSql: fixed(ONE_DAY_RETENTION_BY_TEMPLATE_SQL),
540
+ config: {
541
+ xKey: "date",
542
+ yKey: "rate",
543
+ yFormatter: "percent",
544
+ pivot: {
545
+ xKey: "date",
546
+ seriesKey: "template",
547
+ valueKey: "rate",
548
+ },
549
+ description:
550
+ "Per-template first-seen cohorts in the selected range. A user is retained when they return to the same template the next day.",
551
+ },
552
+ },
553
+ {
554
+ key: "seven-day-retention-by-template",
555
+ title: "7d Retention by Template",
556
+ chartType: "line",
557
+ source: "first-party",
558
+ width: 2,
559
+ windowed: false,
560
+ buildSql: fixed(SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL),
561
+ config: {
562
+ xKey: "date",
563
+ yKey: "rate",
564
+ yFormatter: "percent",
565
+ pivot: {
566
+ xKey: "date",
567
+ seriesKey: "template",
568
+ valueKey: "rate",
569
+ },
570
+ description:
571
+ "Per-template first-seen cohorts in the selected range. A user is retained when they return to the same template seven days later.",
572
+ },
573
+ },
574
+ {
575
+ key: "dau-over-time",
576
+ title: "DAU by Template",
577
+ chartType: "area",
578
+ source: "first-party",
579
+ width: 2,
580
+ windowed: false,
581
+ buildSql: fixed(
582
+ "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) 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')) GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template",
583
+ ),
584
+ config: {
585
+ xKey: "date",
586
+ yKey: "users",
587
+ yFormatter: "number",
588
+ pivot: {
589
+ xKey: "date",
590
+ seriesKey: "template",
591
+ valueKey: "users",
592
+ },
593
+ stacked: true,
594
+ description:
595
+ "Distinct active users per day in the selected time range, stacked by template. Uses user_id first, then anonymous_id.",
596
+ },
597
+ },
598
+ {
599
+ key: "wau-over-time",
600
+ title: "WAU by Template",
601
+ chartType: "area",
602
+ source: "first-party",
603
+ width: 2,
604
+ windowed: false,
605
+ buildSql: fixed(
606
+ "WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL 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'))), days AS (SELECT DISTINCT event_date AS date FROM base 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')))) SELECT d.date, b.template, COUNT(DISTINCT b.user_key) AS users 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",
607
+ ),
608
+ config: {
609
+ xKey: "date",
610
+ yKey: "users",
611
+ yFormatter: "number",
612
+ pivot: {
613
+ xKey: "date",
614
+ seriesKey: "template",
615
+ valueKey: "users",
616
+ },
617
+ stacked: true,
326
618
  description:
327
- "Total first-party tracked events per day (activity proxy there is no distinct pageview event in /track data).",
619
+ "Trailing 7-day distinct active users for each active date in the selected time range, stacked by template.",
328
620
  },
329
621
  },
330
622
 
@@ -3,4 +3,4 @@ type: added
3
3
  date: 2026-06-23
4
4
  ---
5
5
 
6
- Added a heads-up explaining Google's 'app not verified' screen when connecting your calendar, so you know it's safe to continue
6
+ Added a heads-up explaining Google's 'app not verified' screen when signing in or connecting your calendar, so you know it's safe to continue
@@ -16,6 +16,18 @@ export default createAuthPlugin({
16
16
  "Manages availability and booking links automatically",
17
17
  "Answers schedule questions and resolves conflicts instantly",
18
18
  ],
19
+ runLocalCommand:
20
+ "npx @agent-native/core@latest create my-calendar-app --template calendar",
21
+ },
22
+ googleSignInNotice: {
23
+ host: "calendar.agent-native.com",
24
+ title: "Google may show a warning",
25
+ body: [
26
+ "You'll see this screen because this demo uses Agent-Native's Google app, not a Google-reviewed public app.",
27
+ "It's safe to continue: click Advanced, then “Go to … (unsafe)” to finish signing in.",
28
+ ],
29
+ continueLabel: "Continue to Google",
30
+ cancelLabel: "Run locally",
19
31
  },
20
32
  publicPaths: [
21
33
  "/book",
@@ -57,13 +57,13 @@ the recorder page itself. The Clips Chrome extension is the active-tab path for
57
57
  browser logs: it launches `/record` with an extension capture session and passes
58
58
  `developerLogs=1/0`, then saves diagnostics with source `extension`.
59
59
 
60
- Keep the public Chrome extension option hidden until the Web Store listing is
61
- live. UI prompts that otherwise say "Download desktop app" should use the shared
62
- install-choice component so `VITE_CLIPS_CHROME_EXTENSION_ENABLED=1` can reveal
63
- two options: Chrome extension for browser logs, and desktop app for the most
64
- seamless native capture. Also set `VITE_CLIPS_CHROME_EXTENSION_URL` when the
65
- extension URL exists; leave the boolean unset while preparing/submitting the
66
- listing.
60
+ The Web Store listing is live, so the public Chrome extension option shows by
61
+ default. UI prompts that otherwise say "Download desktop app" use the shared
62
+ install-choice component (`CaptureInstallButton` / `CaptureInstallInlineLink`)
63
+ to offer two options: Chrome extension for browser logs, and desktop app for the
64
+ most seamless native capture. Set `VITE_CLIPS_CHROME_EXTENSION_ENABLED=0` to hide
65
+ the Chrome option again, or `VITE_CLIPS_CHROME_EXTENSION_URL` to point at a
66
+ different listing.
67
67
 
68
68
  ## Pause / resume
69
69
 
@@ -65,8 +65,12 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
65
65
  the recording session. `save-browser-diagnostics` is UI/internal and stores
66
66
  bounded console logs plus fetch/XHR method, URL path/query keys, status, and
67
67
  duration; it never captures headers, bodies, cookies, or query values. Use
68
- `get-recording-player-data` for full diagnostics when you have editor access;
69
- public agent context only exposes a compact issue summary.
68
+ `get-recording-player-data` for full diagnostics when you have editor access.
69
+ Public agent context exposes the redacted console stream (all levels) as
70
+ `browserDiagnostics.consoleLogs` and the fetch/XHR stream as
71
+ `browserDiagnostics.networkRequests` (method, sanitized URL with query values
72
+ redacted, status, duration), plus `consoleIssues` and `failedNetworkRequests`
73
+ highlights. All bounded; page URL, headers, bodies, and cookies stay omitted.
70
74
  - The Chrome extension lives in `chrome-extension/`. It launches `/record` with
71
75
  `clipsExtensionId` and `clipsCaptureSessionId`, then the recorder sends
72
76
  `CLIPS_CAPTURE_START/STOP/CANCEL` back to the extension. The extension uses
@@ -1,7 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { Link } from "react-router";
3
3
  import {
4
- IconBrowser,
4
+ IconBrandApple,
5
+ IconBrandChrome,
6
+ IconBrandWindows,
5
7
  IconChevronDown,
6
8
  IconDeviceDesktop,
7
9
  IconExternalLink,
@@ -35,8 +37,22 @@ type CaptureInstallInlineLinkProps = PopoverPlacement & {
35
37
  desktopHref?: string;
36
38
  };
37
39
 
40
+ /**
41
+ * The desktop-app tile shows the icon for the visitor's current OS — Apple on
42
+ * macOS, Windows on Windows — and falls back to a neutral desktop glyph on other
43
+ * platforms or during SSR. The Chrome tile always uses the Chrome brand icon.
44
+ */
45
+ function desktopOsIcon(): typeof IconDeviceDesktop {
46
+ if (typeof navigator === "undefined") return IconDeviceDesktop;
47
+ const ua = navigator.userAgent;
48
+ if (/Windows/i.test(ua)) return IconBrandWindows;
49
+ if (/Mac|iPhone|iPad/i.test(ua)) return IconBrandApple;
50
+ return IconDeviceDesktop;
51
+ }
52
+
38
53
  function InstallOptionsContent({ desktopHref = "/download" }) {
39
54
  const chromeAvailable = Boolean(clipsChromeExtensionUrl);
55
+ const DesktopIcon = desktopOsIcon();
40
56
 
41
57
  return (
42
58
  <div className="grid gap-2">
@@ -57,7 +73,7 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
57
73
  rel="noreferrer"
58
74
  className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
59
75
  >
60
- <IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
76
+ <IconBrandChrome className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
61
77
  <span className="min-w-0 flex-1">
62
78
  <span className="block text-sm font-medium">Chrome extension</span>
63
79
  <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
@@ -69,7 +85,7 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
69
85
  </a>
70
86
  ) : (
71
87
  <div className="flex items-start gap-3 rounded-md border border-dashed border-border p-3 text-left opacity-70">
72
- <IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
88
+ <IconBrandChrome className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
73
89
  <span className="min-w-0 flex-1">
74
90
  <span className="block text-sm font-medium">Chrome extension</span>
75
91
  <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
@@ -83,7 +99,7 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
83
99
  to={desktopHref}
84
100
  className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
85
101
  >
86
- <IconDeviceDesktop className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
102
+ <DesktopIcon className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
87
103
  <span className="min-w-0 flex-1">
88
104
  <span className="block text-sm font-medium">Desktop app</span>
89
105
  <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">