@agent-native/core 0.99.1 → 0.99.3

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 (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +42 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  5. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  6. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  7. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  8. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  9. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  10. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  11. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  12. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  13. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +12 -38
  16. package/corpus/core/src/audit/config.ts +0 -1
  17. package/corpus/core/src/chat-threads/store.ts +57 -41
  18. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  19. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  20. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  23. package/corpus/core/src/client/index.ts +7 -0
  24. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  25. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  26. package/corpus/core/src/client/use-db-sync.ts +3 -15
  27. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  28. package/corpus/core/src/demo/browser-state.ts +47 -0
  29. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  30. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  31. package/corpus/core/src/server/action-discovery.ts +0 -1
  32. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  33. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  34. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  35. package/corpus/templates/analytics/AGENTS.md +4 -4
  36. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  37. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  38. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  39. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  40. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
  45. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  48. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  49. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  51. package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
  52. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  53. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  54. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  55. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  56. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  57. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  58. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  59. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  60. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  61. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  64. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  65. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  66. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  67. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  68. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  69. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  70. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  71. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  72. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  73. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  74. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  75. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  76. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  77. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  78. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  79. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  80. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  81. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +13 -37
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/audit/config.d.ts.map +1 -1
  86. package/dist/audit/config.js +0 -1
  87. package/dist/audit/config.js.map +1 -1
  88. package/dist/chat-threads/store.d.ts.map +1 -1
  89. package/dist/chat-threads/store.js +47 -33
  90. package/dist/chat-threads/store.js.map +1 -1
  91. package/dist/client/AssistantChat.d.ts.map +1 -1
  92. package/dist/client/AssistantChat.js +1 -1
  93. package/dist/client/AssistantChat.js.map +1 -1
  94. package/dist/client/chat/message-components.d.ts.map +1 -1
  95. package/dist/client/chat/message-components.js +9 -3
  96. package/dist/client/chat/message-components.js.map +1 -1
  97. package/dist/client/chat/tool-call-display.d.ts +3 -1
  98. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  99. package/dist/client/chat/tool-call-display.js +10 -2
  100. package/dist/client/chat/tool-call-display.js.map +1 -1
  101. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  102. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  103. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  104. package/dist/client/extensions/ExtensionViewer.js +2 -59
  105. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  106. package/dist/client/index.d.ts +2 -0
  107. package/dist/client/index.d.ts.map +1 -1
  108. package/dist/client/index.js +2 -0
  109. package/dist/client/index.js.map +1 -1
  110. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  111. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  112. package/dist/client/settings/DemoModeSection.js +5 -52
  113. package/dist/client/settings/DemoModeSection.js.map +1 -1
  114. package/dist/client/settings/SettingsPanel.js +1 -1
  115. package/dist/client/settings/SettingsPanel.js.map +1 -1
  116. package/dist/client/use-db-sync.d.ts.map +1 -1
  117. package/dist/client/use-db-sync.js +3 -7
  118. package/dist/client/use-db-sync.js.map +1 -1
  119. package/dist/client/use-demo-mode-status.d.ts +3 -4
  120. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  121. package/dist/client/use-demo-mode-status.js +8 -23
  122. package/dist/client/use-demo-mode-status.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +2 -2
  126. package/dist/collab/struct-routes.d.ts +1 -1
  127. package/dist/demo/browser-state.d.ts +10 -0
  128. package/dist/demo/browser-state.d.ts.map +1 -0
  129. package/dist/demo/browser-state.js +49 -0
  130. package/dist/demo/browser-state.js.map +1 -0
  131. package/dist/demo/fetch-interceptor.d.ts +2 -7
  132. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  133. package/dist/demo/fetch-interceptor.js +12 -56
  134. package/dist/demo/fetch-interceptor.js.map +1 -1
  135. package/dist/deploy/workspace-deploy.js +2 -2
  136. package/dist/deploy/workspace-deploy.js.map +1 -1
  137. package/dist/notifications/routes.d.ts +2 -2
  138. package/dist/observability/routes.d.ts +5 -5
  139. package/dist/resources/handlers.d.ts +3 -3
  140. package/dist/secrets/routes.d.ts +9 -9
  141. package/dist/server/action-discovery.d.ts.map +1 -1
  142. package/dist/server/action-discovery.js +0 -1
  143. package/dist/server/action-discovery.js.map +1 -1
  144. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  145. package/dist/server/core-routes-plugin.js +0 -27
  146. package/dist/server/core-routes-plugin.js.map +1 -1
  147. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  148. package/docs/content/a2a-protocol.mdx +42 -0
  149. package/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  150. package/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  151. package/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  152. package/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  153. package/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  154. package/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  155. package/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  156. package/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  157. package/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  158. package/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  159. package/package.json +1 -1
  160. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  161. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  162. package/corpus/core/src/demo/config.ts +0 -57
  163. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  164. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  165. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  166. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  167. package/dist/demo/config.d.ts +0 -9
  168. package/dist/demo/config.d.ts.map +0 -1
  169. package/dist/demo/config.js +0 -58
  170. package/dist/demo/config.js.map +0 -1
@@ -13,6 +13,7 @@ import { z } from "zod";
13
13
 
14
14
  import { interpolate } from "../app/pages/adhoc/sql-dashboard/interpolate";
15
15
  import { dryRunQuery } from "../server/lib/bigquery";
16
+ import { validateFirstPartyDashboardTimeScope } from "../server/lib/dashboard-time-scope";
16
17
  import {
17
18
  upsertDashboard,
18
19
  upsertDashboardWithRetry,
@@ -421,8 +422,20 @@ export async function validatePanelSql(
421
422
  const raw = typeof p.sql === "string" ? p.sql : "";
422
423
  if (raw.trim()) {
423
424
  try {
425
+ const timeScopeError = validateFirstPartyDashboardTimeScope(
426
+ p,
427
+ config,
428
+ i,
429
+ );
430
+ if (timeScopeError) return timeScopeError;
424
431
  validateFirstPartyAnalyticsSql(interpolate(raw, vars));
425
432
  } catch (e: any) {
433
+ if (
434
+ typeof e?.message === "string" &&
435
+ e.message.startsWith("panel[")
436
+ ) {
437
+ return e.message;
438
+ }
426
439
  return `panel[${i}] "${p.title || p.id}" first-party analytics SQL is invalid: ${e?.message ?? e}`;
427
440
  }
428
441
  }
@@ -552,6 +565,7 @@ export default defineAction({
552
565
  "Use this action when creating a brand-new dashboard from a complete config, for the UI full-config save path, or for an explicitly requested low-level JSON-pointer compatibility edit. " +
553
566
  "Do not use `ops` or `panelOrder` for ordinary agent edits like moving charts, adding panels to an existing dashboard, changing widths, or updating panel config; call `mutate-dashboard` once with the full edit script. " +
554
567
  "When this action is appropriate, provide only one of `ops`, `panelOrder`, or `config`; `config` replaces the whole dashboard config. " +
568
+ "First-party event panels must bind to a declared dashboard time filter with `{{timeRange}}` or date-range variables. Intentional fixed-window, cohort-history, and all-time exceptions must be explicit in `panel.config.timeScope`; unbounded first-party SQL is rejected at save time. " +
555
569
  "To add a shipped catalog template's panels to an existing dashboard, prefer `install-dashboard-template` with `mergePanels: true` — it appends the template's panels in one call without you having to author each panel. " +
556
570
  "The result is compact by default: `panelCount`, `appliedOps`, `panelOrder`, `firstPanelIds`, and `summary`. Set `returnConfig: true` only when you truly need the full config in the tool result. " +
557
571
  "The UI auto-refreshes after this action — do NOT call `refresh-screen`.",
@@ -14,6 +14,7 @@ import {
14
14
  IconReportAnalytics,
15
15
  IconSearch,
16
16
  IconArchive,
17
+ IconActivity,
17
18
  IconHeartbeat,
18
19
  IconPin,
19
20
  IconPlus,
@@ -26,7 +27,6 @@ import {
26
27
  IconPlayerPlay,
27
28
  IconLayoutSidebarLeftCollapse,
28
29
  IconLayoutSidebarLeftExpand,
29
- IconShieldCheck,
30
30
  } from "@tabler/icons-react";
31
31
  import {
32
32
  useQuery,
@@ -97,7 +97,7 @@ import {
97
97
  type ChatThreadSummary,
98
98
  } from "@agent-native/core/client";
99
99
  import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
100
- import { OrgSwitcher, useOrgRole } from "@agent-native/core/client/org";
100
+ import { OrgSwitcher } from "@agent-native/core/client/org";
101
101
 
102
102
  import {
103
103
  AlertDialog,
@@ -1593,7 +1593,6 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
1593
1593
  const navigate = useNavigate();
1594
1594
  const t = useT();
1595
1595
  const queryClient = useQueryClient();
1596
- const { canManageOrg } = useOrgRole();
1597
1596
  const { setTheme } = useTheme();
1598
1597
 
1599
1598
  const isAskRoute = location.pathname === "/ask";
@@ -2428,16 +2427,12 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2428
2427
  href: "/monitoring",
2429
2428
  active: location.pathname.startsWith("/monitoring"),
2430
2429
  },
2431
- ...(canManageOrg
2432
- ? [
2433
- {
2434
- icon: IconShieldCheck,
2435
- label: t("navigation.admin"),
2436
- href: "/agents?view=dashboards",
2437
- active: location.pathname.startsWith("/agents"),
2438
- },
2439
- ]
2440
- : []),
2430
+ {
2431
+ icon: IconActivity,
2432
+ label: t("navigation.agents"),
2433
+ href: "/agents",
2434
+ active: location.pathname.startsWith("/agents"),
2435
+ },
2441
2436
  {
2442
2437
  icon: IconDatabase,
2443
2438
  label: t("navigation.dataSources"),
@@ -2641,20 +2636,19 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2641
2636
  {t("navigation.monitoring")}
2642
2637
  </Link>
2643
2638
 
2644
- {canManageOrg ? (
2645
- <Link
2646
- to="/agents?view=dashboards"
2647
- className={cn(
2648
- "order-6 flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:text-primary",
2649
- location.pathname.startsWith("/agents")
2650
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
2651
- : "text-muted-foreground hover:bg-sidebar-accent/50",
2652
- )}
2653
- >
2654
- <IconShieldCheck className="h-4 w-4" />
2655
- {t("navigation.admin")}
2656
- </Link>
2657
- ) : null}
2639
+ {/* Agents link */}
2640
+ <Link
2641
+ to="/agents"
2642
+ className={cn(
2643
+ "order-6 flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:text-primary",
2644
+ location.pathname.startsWith("/agents")
2645
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
2646
+ : "text-muted-foreground hover:bg-sidebar-accent/50",
2647
+ )}
2648
+ >
2649
+ <IconActivity className="h-4 w-4" />
2650
+ {t("navigation.agents")}
2651
+ </Link>
2658
2652
 
2659
2653
  {/* Data Sources link */}
2660
2654
  <Link
@@ -297,7 +297,10 @@ const PanelCell = memo(function PanelCell({
297
297
  [panel, vars],
298
298
  );
299
299
  const resolvedSql = useMemo(
300
- () => interpolate(serializePanelSql(panel.sql), vars),
300
+ () =>
301
+ interpolate(serializePanelSql(panel.sql), vars, {
302
+ failClosedTimeVariables: true,
303
+ }),
301
304
  [panel.sql, vars],
302
305
  );
303
306
  const handleSelectForChat = useCallback(
@@ -14,9 +14,23 @@ function escapeSqlValue(value: string): string {
14
14
  return value.replace(/'/g, "''");
15
15
  }
16
16
 
17
+ export interface InterpolateOptions {
18
+ /**
19
+ * Replace missing/empty time variables with a value that cannot satisfy the
20
+ * canonical dashboard predicates. This keeps a stale or malformed panel
21
+ * fail-closed at render time instead of turning into an all-time query.
22
+ */
23
+ failClosedTimeVariables?: boolean;
24
+ }
25
+
26
+ function isTimeVariable(name: string): boolean {
27
+ return name === "timeRange" || /(?:Start|End)$/.test(name);
28
+ }
29
+
17
30
  export function interpolate(
18
31
  sql: string | undefined | null,
19
32
  vars: Record<string, string> = {},
33
+ options: InterpolateOptions = {},
20
34
  ): string {
21
35
  // Defensive: a malformed dashboard config (e.g. agent wrote a panel without
22
36
  // a `sql` field) should not crash the page. Treat missing/non-string SQL
@@ -33,6 +47,13 @@ export function interpolate(
33
47
 
34
48
  return withConditionals.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
35
49
  const value = vars[name];
50
+ if (
51
+ options.failClosedTimeVariables &&
52
+ isTimeVariable(name) &&
53
+ (value == null || value.length === 0)
54
+ ) {
55
+ return "__missing_dashboard_time_filter__";
56
+ }
36
57
  if (value == null) return "";
37
58
  return escapeSqlValue(String(value));
38
59
  });
@@ -63,7 +63,21 @@ export interface PivotConfig {
63
63
  valueKey: string;
64
64
  }
65
65
 
66
+ /**
67
+ * Declares how a query's time coverage relates to the dashboard filter.
68
+ * `dashboard` is the default for ordinary event metrics; the other values are
69
+ * explicit exceptions that make intentional history scans visible to agents
70
+ * and reviewers.
71
+ */
72
+ export type DashboardTimeScope =
73
+ | "dashboard"
74
+ | "fixed-window"
75
+ | "cohort-history"
76
+ | "all-time";
77
+
66
78
  export interface SqlPanelConfig {
79
+ /** Time coverage contract for first-party SQL panels. */
80
+ timeScope?: DashboardTimeScope;
67
81
  xKey?: string;
68
82
  yKey?: string;
69
83
  yKeys?: string[];
@@ -47,6 +47,13 @@ import { Link, useParams } from "react-router";
47
47
 
48
48
  import { Button } from "@/components/ui/button";
49
49
  import { Card, CardContent } from "@/components/ui/card";
50
+ import {
51
+ DropdownMenu,
52
+ DropdownMenuContent,
53
+ DropdownMenuRadioGroup,
54
+ DropdownMenuRadioItem,
55
+ DropdownMenuTrigger,
56
+ } from "@/components/ui/dropdown-menu";
50
57
  import { Input } from "@/components/ui/input";
51
58
  import {
52
59
  Popover,
@@ -1087,22 +1094,34 @@ function ReplayPlayer({
1087
1094
  {formatClock(totalTime)}
1088
1095
  </span>
1089
1096
 
1090
- <div className="flex items-center rounded-md bg-muted p-1">
1091
- {SPEED_OPTIONS.map((option) => (
1097
+ <DropdownMenu>
1098
+ <DropdownMenuTrigger asChild>
1092
1099
  <button
1093
- key={option}
1094
1100
  type="button"
1095
- className={cn(
1096
- "rounded px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
1097
- speed === option &&
1098
- "bg-background text-foreground shadow-sm",
1099
- )}
1100
- onClick={() => updateSpeed(option)}
1101
+ disabled={disabled}
1102
+ aria-label={`${speed}x`}
1103
+ className="inline-flex h-8 min-w-11 items-center justify-center rounded-md border px-2 text-xs font-medium tabular-nums transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
1101
1104
  >
1102
- {option}x
1105
+ {speed}x
1103
1106
  </button>
1104
- ))}
1105
- </div>
1107
+ </DropdownMenuTrigger>
1108
+ <DropdownMenuContent align="center" side="top">
1109
+ <DropdownMenuRadioGroup
1110
+ value={String(speed)}
1111
+ onValueChange={(value) => updateSpeed(Number(value))}
1112
+ >
1113
+ {SPEED_OPTIONS.map((option) => (
1114
+ <DropdownMenuRadioItem
1115
+ key={option}
1116
+ value={String(option)}
1117
+ className="tabular-nums"
1118
+ >
1119
+ {option}x
1120
+ </DropdownMenuRadioItem>
1121
+ ))}
1122
+ </DropdownMenuRadioGroup>
1123
+ </DropdownMenuContent>
1124
+ </DropdownMenu>
1106
1125
 
1107
1126
  <Tooltip>
1108
1127
  <TooltipTrigger asChild>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Dashboard charts now honor the selected time range by default and reject accidental unbounded first-party queries
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Keep real authorized identities in Analytics incident lookups when browser Demo mode is enabled
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ The Analytics Agents tab is available again from the main navigation.
@@ -226,7 +226,7 @@
226
226
  "chartType": "bar",
227
227
  "source": "first-party",
228
228
  "width": 2,
229
- "sql": "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
229
+ "sql": "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' 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'))) 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(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
230
230
  "config": {
231
231
  "xKey": "app",
232
232
  "yKey": "count",
@@ -240,7 +240,7 @@
240
240
  "chartType": "bar",
241
241
  "source": "first-party",
242
242
  "width": 1,
243
- "sql": "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
243
+ "sql": "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' 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'))) 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(signed_in, ''), 'unknown') ORDER BY signed_in",
244
244
  "config": {
245
245
  "xKey": "signed_in",
246
246
  "yKey": "count",
@@ -254,7 +254,7 @@
254
254
  "chartType": "area",
255
255
  "source": "first-party",
256
256
  "width": 1,
257
- "sql": "SELECT event_date AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY event_date ORDER BY date",
257
+ "sql": "SELECT event_date AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' 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'))) 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 event_date ORDER BY date",
258
258
  "config": {
259
259
  "xKey": "date",
260
260
  "yKey": "count",
@@ -56,6 +56,10 @@ function panelSummaries(config: Record<string, unknown>) {
56
56
  width: typeof panel.width === "number" ? panel.width : undefined,
57
57
  columns: typeof panel.columns === "number" ? panel.columns : undefined,
58
58
  tab: typeof panel.tab === "string" ? panel.tab : undefined,
59
+ timeScope:
60
+ typeof panelConfig.timeScope === "string"
61
+ ? panelConfig.timeScope
62
+ : undefined,
59
63
  description:
60
64
  typeof panelConfig.description === "string"
61
65
  ? panelConfig.description
@@ -0,0 +1,170 @@
1
+ import type { DashboardTimeScope } from "../../app/pages/adhoc/sql-dashboard/types";
2
+
3
+ export const DASHBOARD_TIME_SCOPES: readonly DashboardTimeScope[] = [
4
+ "dashboard",
5
+ "fixed-window",
6
+ "cohort-history",
7
+ "all-time",
8
+ ];
9
+
10
+ type DashboardFilterLike = {
11
+ id?: unknown;
12
+ key?: unknown;
13
+ type?: unknown;
14
+ default?: unknown;
15
+ };
16
+
17
+ type DashboardPanelLike = {
18
+ id?: unknown;
19
+ title?: unknown;
20
+ chartType?: unknown;
21
+ source?: unknown;
22
+ sql?: unknown;
23
+ config?: unknown;
24
+ };
25
+
26
+ type DashboardConfigLike = {
27
+ filters?: unknown;
28
+ };
29
+
30
+ const TEMPORAL_VARIABLE_RE = /\{\{(timeRange|[A-Za-z_]\w*(?:Start|End))\}\}/g;
31
+
32
+ /** Return the time variables a panel actually references. */
33
+ export function extractDashboardTimeVariables(sql: string): string[] {
34
+ const variables = new Set<string>();
35
+ for (const match of sql.matchAll(TEMPORAL_VARIABLE_RE)) {
36
+ variables.add(match[1]);
37
+ }
38
+ return [...variables];
39
+ }
40
+
41
+ function filterId(filter: DashboardFilterLike): string {
42
+ const value = filter.id ?? filter.key;
43
+ return typeof value === "string" ? value.trim() : "";
44
+ }
45
+
46
+ function filtersFrom(config: DashboardConfigLike): DashboardFilterLike[] {
47
+ return Array.isArray(config.filters)
48
+ ? (config.filters as DashboardFilterLike[])
49
+ : [];
50
+ }
51
+
52
+ function panelConfig(panel: DashboardPanelLike): Record<string, unknown> {
53
+ return panel.config && typeof panel.config === "object"
54
+ ? (panel.config as Record<string, unknown>)
55
+ : {};
56
+ }
57
+
58
+ function hasExplicitLowerBound(sql: string): boolean {
59
+ // This intentionally recognizes the bounded shapes used by the shipped
60
+ // first-party catalog. It is a compatibility escape hatch for fixed-window
61
+ // catalog metrics; new ad-hoc panels should use a dashboard placeholder.
62
+ return /\b(?:event_date|timestamp|started_at|ended_at|cohort_date|created_at|date)\b[\s\S]{0,100}?(?:>=|>)\s*[\s\S]{0,160}?(?:CURRENT_DATE|CURRENT_TIMESTAMP|NOW\s*\(|INTERVAL\s*['"]|DATE\s*['"]|TIMESTAMP\s*['"]|\b20\d{2}-\d{2}-\d{2}\b)/i.test(
63
+ sql,
64
+ );
65
+ }
66
+
67
+ function hasIntentionalHistoryDescription(
68
+ panel: DashboardPanelLike,
69
+ config: Record<string, unknown>,
70
+ ): boolean {
71
+ const values = [panel.title, config.description].filter(
72
+ (value): value is string => typeof value === "string",
73
+ );
74
+ return /all[- ]?time|lifetime|histor(?:y|ical)/i.test(values.join(" "));
75
+ }
76
+
77
+ function scopeValue(panel: DashboardPanelLike): DashboardTimeScope | undefined {
78
+ const value = panelConfig(panel).timeScope;
79
+ return typeof value === "string" &&
80
+ (DASHBOARD_TIME_SCOPES as readonly string[]).includes(value)
81
+ ? (value as DashboardTimeScope)
82
+ : undefined;
83
+ }
84
+
85
+ /**
86
+ * Validate the temporal contract for a first-party dashboard panel.
87
+ *
88
+ * Ordinary panels must bind to a dashboard filter. Intentional exceptions are
89
+ * explicit in `config.timeScope`, while legacy fixed-window catalog SQL is
90
+ * accepted when its lower bound is visible in the SQL itself.
91
+ */
92
+ export function validateFirstPartyDashboardTimeScope(
93
+ panel: DashboardPanelLike,
94
+ dashboard: DashboardConfigLike,
95
+ index: number,
96
+ ): string | null {
97
+ if (panel.source !== "first-party") return null;
98
+ if (panel.chartType === "section" || panel.chartType === "extension") {
99
+ return null;
100
+ }
101
+
102
+ const sql = typeof panel.sql === "string" ? panel.sql : "";
103
+ const variables = extractDashboardTimeVariables(sql);
104
+ const filters = filtersFrom(dashboard);
105
+ const label =
106
+ typeof panel.title === "string" && panel.title.trim()
107
+ ? `"${panel.title}"`
108
+ : `at index ${index}`;
109
+
110
+ for (const variable of variables) {
111
+ const expectedFilterId =
112
+ variable === "timeRange"
113
+ ? "timeRange"
114
+ : variable.replace(/(?:Start|End)$/, "");
115
+ const filter = filters.find(
116
+ (candidate) => filterId(candidate) === expectedFilterId,
117
+ );
118
+ if (!filter) {
119
+ return `panel[${index}] ${label} uses {{${variable}}} but config.filters has no matching "${expectedFilterId}" filter; bind first-party SQL to a declared dashboard time filter`;
120
+ }
121
+ if (variable === "timeRange") {
122
+ if (filter.type !== "select") {
123
+ return `panel[${index}] ${label} uses {{timeRange}}, but filter "timeRange" must have type "select"`;
124
+ }
125
+ } else if (filter.type !== "date-range") {
126
+ return `panel[${index}] ${label} uses {{${variable}}}, but filter "${expectedFilterId}" must have type "date-range"`;
127
+ }
128
+ if (typeof filter.default !== "string" || !filter.default.trim()) {
129
+ return `panel[${index}] ${label} uses a time filter without a non-empty default; choose a bounded default such as "90d" so missing filter state cannot become all-time`;
130
+ }
131
+ }
132
+
133
+ const configuredScope = panelConfig(panel).timeScope;
134
+ if (
135
+ configuredScope !== undefined &&
136
+ (typeof configuredScope !== "string" ||
137
+ !(DASHBOARD_TIME_SCOPES as readonly string[]).includes(configuredScope))
138
+ ) {
139
+ return `panel[${index}] ${label} config.timeScope must be one of ${DASHBOARD_TIME_SCOPES.join(", ")}`;
140
+ }
141
+
142
+ const scope = scopeValue(panel);
143
+ const hasDashboardBinding = variables.length > 0;
144
+ const hasLowerBound = hasExplicitLowerBound(sql);
145
+
146
+ if (scope === "dashboard" && !hasDashboardBinding) {
147
+ return `panel[${index}] ${label} declares timeScope "dashboard" but does not reference {{timeRange}} or a date-range variable`;
148
+ }
149
+ if (scope === "fixed-window" && hasDashboardBinding) {
150
+ return `panel[${index}] ${label} declares timeScope "fixed-window" but uses dashboard time variables; use timeScope "dashboard" or remove the placeholders`;
151
+ }
152
+ if (scope === "fixed-window" && !hasLowerBound) {
153
+ return `panel[${index}] ${label} declares timeScope "fixed-window" without a recognizable lower date bound`;
154
+ }
155
+ if (scope === "all-time" && hasDashboardBinding) {
156
+ return `panel[${index}] ${label} declares timeScope "all-time" but uses dashboard time variables; use timeScope "dashboard" for filter-bound output`;
157
+ }
158
+ if (
159
+ scope === "all-time" &&
160
+ !hasIntentionalHistoryDescription(panel, panelConfig(panel))
161
+ ) {
162
+ return `panel[${index}] ${label} declares timeScope "all-time"; add a description or title that says lifetime, historical, or all-time so the exception is explicit`;
163
+ }
164
+
165
+ if (!scope && !hasDashboardBinding && !hasLowerBound) {
166
+ return `panel[${index}] ${label} reads first-party analytics without a time bound; use {{timeRange}} with a non-empty default filter, or explicitly set config.timeScope to "cohort-history" or "all-time" for intentional history scans`;
167
+ }
168
+
169
+ return null;
170
+ }
@@ -18,7 +18,6 @@ import { fileURLToPath } from "node:url";
18
18
  * reads go through `accessFilter` so an org-scoped key surfaces its issues to
19
19
  * the whole org exactly like session recordings.
20
20
  */
21
- import { appStateGet } from "@agent-native/core/application-state";
22
21
  import { notifyWithDelivery } from "@agent-native/core/notifications";
23
22
  import { recordChange } from "@agent-native/core/server";
24
23
  import { accessFilter } from "@agent-native/core/sharing";
@@ -1099,56 +1098,6 @@ export interface ListErrorIssuesFilters {
1099
1098
  limit?: number;
1100
1099
  }
1101
1100
 
1102
- export const ERROR_REPORTING_ANONYMOUS_EMAIL = "anonymous@builder.io";
1103
- const ERROR_REPORTING_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
1104
-
1105
- /**
1106
- * Error reports contain emails in identity fields, messages, stacks, URLs,
1107
- * tags, extra context, and breadcrumbs. Redact every string at this owning
1108
- * read seam so demo mode never depends on browser interception timing.
1109
- */
1110
- export function anonymizeErrorReportingEmails<T>(value: T): T {
1111
- if (typeof value === "string") {
1112
- return value.replace(
1113
- ERROR_REPORTING_EMAIL_PATTERN,
1114
- ERROR_REPORTING_ANONYMOUS_EMAIL,
1115
- ) as T;
1116
- }
1117
- if (Array.isArray(value)) {
1118
- return value.map((item) => anonymizeErrorReportingEmails(item)) as T;
1119
- }
1120
- if (value && typeof value === "object") {
1121
- if (value instanceof Date) return value;
1122
- const output: Record<string, unknown> = {};
1123
- for (const [key, entry] of Object.entries(value)) {
1124
- output[key] = anonymizeErrorReportingEmails(entry);
1125
- }
1126
- return output as T;
1127
- }
1128
- return value;
1129
- }
1130
-
1131
- async function isErrorDemoModeEnabled(
1132
- userEmail?: string | null,
1133
- ): Promise<boolean> {
1134
- if (process.env.DEMO_MODE === "true") return true;
1135
- if (!userEmail) return false;
1136
- try {
1137
- // Keep demo state tied to the caller whose issues are being read.
1138
- const state = await appStateGet(userEmail, "demo-mode");
1139
- return state?.enabled === true;
1140
- } catch {
1141
- return false;
1142
- }
1143
- }
1144
-
1145
- function anonymizeErrorReportingEmailsInDemoMode<T>(
1146
- value: T,
1147
- enabled: boolean,
1148
- ): T {
1149
- return enabled ? anonymizeErrorReportingEmails(value) : value;
1150
- }
1151
-
1152
1101
  export interface ErrorIssueSummary {
1153
1102
  id: string;
1154
1103
  fingerprint: string;
@@ -1221,7 +1170,6 @@ export async function listErrorIssues(
1221
1170
  scope: ErrorReadScope,
1222
1171
  filters: ListErrorIssuesFilters = {},
1223
1172
  ): Promise<ErrorIssueSummary[]> {
1224
- const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1225
1173
  const db = getDb() as any;
1226
1174
  const limit = Math.min(
1227
1175
  MAX_ISSUE_LIMIT,
@@ -1312,7 +1260,7 @@ export async function listErrorIssues(
1312
1260
  template: row.template ?? null,
1313
1261
  sparkline: sparklines.get(row.id) ?? new Array(SPARKLINE_DAYS).fill(0),
1314
1262
  }));
1315
- return anonymizeErrorReportingEmailsInDemoMode(issues, await demoModeEnabled);
1263
+ return issues;
1316
1264
  }
1317
1265
 
1318
1266
  export interface ErrorEventDetail {
@@ -1404,7 +1352,6 @@ export async function getErrorIssue(
1404
1352
  issueId: string,
1405
1353
  options: { eventsLimit?: number } = {},
1406
1354
  ): Promise<ErrorIssueDetail> {
1407
- const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1408
1355
  const db = getDb() as any;
1409
1356
  const [issueRow] = await db
1410
1357
  .select()
@@ -1530,14 +1477,11 @@ export async function getErrorIssue(
1530
1477
  sparkline: sparklines.get(issueId) ?? new Array(SPARKLINE_DAYS).fill(0),
1531
1478
  };
1532
1479
 
1533
- return anonymizeErrorReportingEmailsInDemoMode(
1534
- {
1535
- issue,
1536
- events,
1537
- sessions: Array.from(sessions.values()),
1538
- },
1539
- await demoModeEnabled,
1540
- );
1480
+ return {
1481
+ issue,
1482
+ events,
1483
+ sessions: Array.from(sessions.values()),
1484
+ };
1541
1485
  }
1542
1486
 
1543
1487
  export interface UpdateErrorIssueInput {