@agent-native/core 0.99.1 → 0.99.2
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/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +12 -38
- package/corpus/core/src/audit/config.ts +0 -1
- package/corpus/core/src/chat-threads/store.ts +57 -41
- package/corpus/core/src/client/AssistantChat.tsx +1 -2
- package/corpus/core/src/client/chat/message-components.tsx +15 -3
- package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
- package/corpus/core/src/client/index.ts +7 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
- package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
- package/corpus/core/src/client/use-db-sync.ts +3 -15
- package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
- package/corpus/core/src/demo/browser-state.ts +47 -0
- package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
- package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
- package/corpus/core/src/server/action-discovery.ts +0 -1
- package/corpus/core/src/server/core-routes-plugin.ts +0 -30
- package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
- package/corpus/templates/analytics/AGENTS.md +4 -4
- package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
- package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
- package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
- package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/assets/app/routes/library.tsx +3 -4
- package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
- package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
- package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
- package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
- package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
- package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
- package/corpus/templates/design/app/pages/Index.tsx +6 -1
- package/corpus/templates/design/app/pages/Present.tsx +12 -6
- package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
- package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
- package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +13 -37
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/audit/config.d.ts.map +1 -1
- package/dist/audit/config.js +0 -1
- package/dist/audit/config.js.map +1 -1
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +47 -33
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +9 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +3 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +10 -2
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +2 -6
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +2 -59
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +1 -13
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +5 -52
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +3 -7
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-demo-mode-status.d.ts +3 -4
- package/dist/client/use-demo-mode-status.d.ts.map +1 -1
- package/dist/client/use-demo-mode-status.js +8 -23
- package/dist/client/use-demo-mode-status.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/browser-state.d.ts +10 -0
- package/dist/demo/browser-state.d.ts.map +1 -0
- package/dist/demo/browser-state.js +49 -0
- package/dist/demo/browser-state.js.map +1 -0
- package/dist/demo/fetch-interceptor.d.ts +2 -7
- package/dist/demo/fetch-interceptor.d.ts.map +1 -1
- package/dist/demo/fetch-interceptor.js +12 -56
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +2 -2
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +0 -1
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +0 -27
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
- package/corpus/core/src/demo/config.ts +0 -57
- package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
- package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
- package/dist/demo/actions/toggle-demo-mode.js +0 -21
- package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
- package/dist/demo/config.d.ts +0 -9
- package/dist/demo/config.d.ts.map +0 -1
- package/dist/demo/config.js +0 -58
- package/dist/demo/config.js.map +0 -1
|
@@ -1,61 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <DemoModeSection /> — toggle that replaces emails and numbers with realistic
|
|
3
|
-
* fake data everywhere (UI + what the agent sees) while preserving names,
|
|
4
|
-
* free text, labels, IDs, and structure so the app keeps working.
|
|
5
|
-
*
|
|
6
|
-
* The toggle is stored in application_state under `demo-mode`
|
|
7
|
-
* (`{ enabled: boolean }`) and written via `PUT
|
|
8
|
-
* /_agent-native/application-state/demo-mode` (optimistic — flip first,
|
|
9
|
-
* write in the background). It READS from `/_agent-native/demo/status`
|
|
10
|
-
* instead, which ORs the per-user toggle with the `DEMO_MODE` env: a hosted
|
|
11
|
-
* demo deployment is forced on and the switch is locked so it can't be
|
|
12
|
-
* accidentally turned off on stage.
|
|
13
|
-
*/
|
|
1
|
+
/** Browser-local presentation toggle. Backend and agent results stay real. */
|
|
14
2
|
|
|
15
3
|
import { IconEyeOff } from "@tabler/icons-react";
|
|
16
|
-
import { useEffect, useState } from "react";
|
|
17
4
|
|
|
18
|
-
import {
|
|
5
|
+
import { setBrowserDemoModeEnabled } from "../../demo/browser-state.js";
|
|
19
6
|
import { useDemoModeStatus } from "../use-demo-mode-status.js";
|
|
20
7
|
|
|
21
|
-
const DEMO_MODE_WRITE_URL = agentNativePath(
|
|
22
|
-
"/_agent-native/application-state/demo-mode",
|
|
23
|
-
);
|
|
24
|
-
|
|
25
8
|
export function DemoModeSection() {
|
|
26
|
-
const
|
|
27
|
-
const { enabled: serverEnabled, forced, isLoading } = useDemoModeStatus();
|
|
28
|
-
|
|
29
|
-
// Surface the server value once it arrives (and on subsequent polls), but
|
|
30
|
-
// never clobber an in-flight optimistic toggle with a stale read. When
|
|
31
|
-
// forced by env, always reflect the locked-on state.
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (forced) {
|
|
34
|
-
setEnabled(true);
|
|
35
|
-
} else if (!isLoading) {
|
|
36
|
-
setEnabled((prev) => (prev === null ? serverEnabled : prev));
|
|
37
|
-
}
|
|
38
|
-
}, [serverEnabled, forced, isLoading]);
|
|
39
|
-
|
|
40
|
-
const toggle = async (next: boolean) => {
|
|
41
|
-
if (forced) return;
|
|
42
|
-
const previous = enabled;
|
|
43
|
-
// Optimistic: flip immediately, write in the background.
|
|
44
|
-
setEnabled(next);
|
|
45
|
-
try {
|
|
46
|
-
const res = await fetch(DEMO_MODE_WRITE_URL, {
|
|
47
|
-
method: "PUT",
|
|
48
|
-
headers: { "Content-Type": "application/json" },
|
|
49
|
-
credentials: "include",
|
|
50
|
-
body: JSON.stringify({ enabled: next }),
|
|
51
|
-
});
|
|
52
|
-
if (!res.ok) {
|
|
53
|
-
throw new Error(`HTTP ${res.status}`);
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
setEnabled(previous);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
9
|
+
const { enabled } = useDemoModeStatus();
|
|
59
10
|
|
|
60
11
|
return (
|
|
61
12
|
<div className="flex items-start justify-between gap-3 rounded-md border border-border bg-accent/30 px-2.5 py-2">
|
|
@@ -64,30 +15,23 @@ export function DemoModeSection() {
|
|
|
64
15
|
Enable demo mode
|
|
65
16
|
</div>
|
|
66
17
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
67
|
-
Anonymize emails in
|
|
68
|
-
presentations.
|
|
69
|
-
|
|
18
|
+
Anonymize displayed emails in this browser and reshape supported
|
|
19
|
+
dashboard charts for presentations. Backend, MCP, and agent results
|
|
20
|
+
stay real and access-scoped.
|
|
70
21
|
</p>
|
|
71
|
-
{forced && (
|
|
72
|
-
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
73
|
-
Enforced by the <code>DEMO_MODE</code> environment variable — can't
|
|
74
|
-
be turned off here.
|
|
75
|
-
</p>
|
|
76
|
-
)}
|
|
77
22
|
</div>
|
|
78
23
|
<button
|
|
79
24
|
type="button"
|
|
80
25
|
role="switch"
|
|
81
|
-
aria-checked={
|
|
26
|
+
aria-checked={enabled}
|
|
82
27
|
aria-label="Enable demo mode"
|
|
83
|
-
|
|
84
|
-
onClick={() => toggle(!enabled)}
|
|
28
|
+
onClick={() => setBrowserDemoModeEnabled(!enabled)}
|
|
85
29
|
// Theme tokens; streaming agent owns layout.
|
|
86
30
|
className={`relative inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full transition-colors ${
|
|
87
31
|
enabled
|
|
88
32
|
? "bg-primary"
|
|
89
33
|
: "bg-muted-foreground/30 hover:bg-muted-foreground/50"
|
|
90
|
-
}
|
|
34
|
+
}`}
|
|
91
35
|
>
|
|
92
36
|
<span
|
|
93
37
|
className={`inline-block h-3 w-3 transform rounded-full bg-background transition-transform ${
|
|
@@ -2790,7 +2790,7 @@ function SettingsPanelContent({
|
|
|
2790
2790
|
id={settingsSectionDomId("demo-mode")}
|
|
2791
2791
|
icon={<IconEyeOff size={14} />}
|
|
2792
2792
|
title="Demo mode"
|
|
2793
|
-
subtitle="Replace emails
|
|
2793
|
+
subtitle="Replace displayed emails with realistic fake data in this browser and reshape supported charts for presentations. Backend, MCP, and agent results stay real and access-scoped."
|
|
2794
2794
|
open={openSection === "demo-mode"}
|
|
2795
2795
|
onToggle={() => toggle("demo-mode")}
|
|
2796
2796
|
>
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
ensureDemoModeFetchInterceptor,
|
|
5
|
-
refreshDemoModeFetchInterceptor,
|
|
6
|
-
} from "../demo/fetch-interceptor.js";
|
|
3
|
+
import { ensureDemoModeFetchInterceptor } from "../demo/fetch-interceptor.js";
|
|
7
4
|
import { agentNativePath } from "./api-path.js";
|
|
8
5
|
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
9
6
|
import {
|
|
@@ -314,15 +311,6 @@ class SyncTransport {
|
|
|
314
311
|
// -------------------------------------------------------------------------
|
|
315
312
|
|
|
316
313
|
private fan(events: SyncEvent[], version: number | undefined): void {
|
|
317
|
-
if (
|
|
318
|
-
events.some(
|
|
319
|
-
(event) =>
|
|
320
|
-
event.source === "app-state" &&
|
|
321
|
-
(event.key === "demo-mode" || event.key === "*"),
|
|
322
|
-
)
|
|
323
|
-
) {
|
|
324
|
-
void refreshDemoModeFetchInterceptor();
|
|
325
|
-
}
|
|
326
314
|
for (const sub of this.subscribers.values()) {
|
|
327
315
|
sub.onEvents(events, version);
|
|
328
316
|
}
|
|
@@ -544,8 +532,8 @@ class SyncTransport {
|
|
|
544
532
|
};
|
|
545
533
|
|
|
546
534
|
private start(): void {
|
|
547
|
-
// Universal demo-mode redaction
|
|
548
|
-
// a no-op until
|
|
535
|
+
// Universal browser-local demo-mode presentation redaction. Idempotent and
|
|
536
|
+
// a no-op until the local preference is on. Lives here because every root
|
|
549
537
|
// already mounts useDbSync, so this needs zero per-template wiring.
|
|
550
538
|
ensureEmbedAuthFetchInterceptor();
|
|
551
539
|
ensureDemoModeFetchInterceptor();
|
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
enabled?: boolean;
|
|
8
|
-
forced?: boolean;
|
|
9
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
getBrowserDemoModeEnabled,
|
|
5
|
+
subscribeToBrowserDemoMode,
|
|
6
|
+
} from "../demo/browser-state.js";
|
|
10
7
|
|
|
11
8
|
export interface DemoModeStatus {
|
|
12
9
|
enabled: boolean;
|
|
13
|
-
forced:
|
|
10
|
+
forced: false;
|
|
14
11
|
isLoading: boolean;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
|
|
18
|
-
|
|
19
14
|
/**
|
|
20
|
-
* Reads the
|
|
21
|
-
*
|
|
22
|
-
* so mounted consumers refresh without polling or duplicating the route call.
|
|
15
|
+
* Reads the browser-local Demo mode presentation preference. This deliberately
|
|
16
|
+
* has no backend request: the server and agent always operate on real data.
|
|
23
17
|
*/
|
|
24
18
|
export function useDemoModeStatus(): DemoModeStatus {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const res = await fetch(DEMO_STATUS_URL, {
|
|
31
|
-
credentials: "same-origin",
|
|
32
|
-
});
|
|
33
|
-
if (!res.ok) return null;
|
|
34
|
-
return (await res.json()) as DemoModeStatusResponse | null;
|
|
35
|
-
},
|
|
36
|
-
staleTime: Infinity,
|
|
37
|
-
});
|
|
19
|
+
const enabled = useSyncExternalStore(
|
|
20
|
+
subscribeToBrowserDemoMode,
|
|
21
|
+
getBrowserDemoModeEnabled,
|
|
22
|
+
() => false,
|
|
23
|
+
);
|
|
38
24
|
|
|
39
25
|
return {
|
|
40
|
-
enabled
|
|
41
|
-
forced:
|
|
42
|
-
isLoading,
|
|
26
|
+
enabled,
|
|
27
|
+
forced: false,
|
|
28
|
+
isLoading: false,
|
|
43
29
|
};
|
|
44
30
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Browser-local Demo mode state.
|
|
2
|
+
*
|
|
3
|
+
* Demo mode is a presentation preference for this browser only. It must not
|
|
4
|
+
* be persisted in application state or consulted by backend actions.
|
|
5
|
+
*/
|
|
6
|
+
export const DEMO_MODE_STORAGE_KEY = "agent-native:demo-mode";
|
|
7
|
+
|
|
8
|
+
const DEMO_MODE_CHANGE_EVENT = "agent-native:demo-mode-change";
|
|
9
|
+
|
|
10
|
+
export function getBrowserDemoModeEnabled(): boolean {
|
|
11
|
+
if (typeof window === "undefined") return false;
|
|
12
|
+
try {
|
|
13
|
+
return window.localStorage.getItem(DEMO_MODE_STORAGE_KEY) === "true";
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setBrowserDemoModeEnabled(enabled: boolean): void {
|
|
20
|
+
if (typeof window === "undefined") return;
|
|
21
|
+
try {
|
|
22
|
+
if (enabled) {
|
|
23
|
+
window.localStorage.setItem(DEMO_MODE_STORAGE_KEY, "true");
|
|
24
|
+
} else {
|
|
25
|
+
window.localStorage.removeItem(DEMO_MODE_STORAGE_KEY);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// A blocked or unavailable localStorage should not break the settings UI.
|
|
29
|
+
}
|
|
30
|
+
window.dispatchEvent(new Event(DEMO_MODE_CHANGE_EVENT));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function subscribeToBrowserDemoMode(callback: () => void): () => void {
|
|
34
|
+
if (typeof window === "undefined") return () => undefined;
|
|
35
|
+
|
|
36
|
+
const onStorage = (event: StorageEvent) => {
|
|
37
|
+
if (event.key === DEMO_MODE_STORAGE_KEY || event.key === null) callback();
|
|
38
|
+
};
|
|
39
|
+
const onChange = () => callback();
|
|
40
|
+
|
|
41
|
+
window.addEventListener("storage", onStorage);
|
|
42
|
+
window.addEventListener(DEMO_MODE_CHANGE_EVENT, onChange);
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener("storage", onStorage);
|
|
45
|
+
window.removeEventListener(DEMO_MODE_CHANGE_EVENT, onChange);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { agentNativePath } from "../client/api-path.js";
|
|
2
1
|
/**
|
|
3
2
|
* Client-side demo-mode redaction.
|
|
4
3
|
*
|
|
@@ -14,8 +13,8 @@ import { agentNativePath } from "../client/api-path.js";
|
|
|
14
13
|
* can only ever post-process JSON the app already parses for display, so it
|
|
15
14
|
* physically cannot break auth, SSE streams, SSR HTML, or binary downloads.
|
|
16
15
|
*
|
|
17
|
-
* The agent is
|
|
18
|
-
*
|
|
16
|
+
* The agent transport is intentionally skipped because it carries the real
|
|
17
|
+
* backend results; this interceptor is only a browser presentation transform.
|
|
19
18
|
*
|
|
20
19
|
* Scope intentionally narrow:
|
|
21
20
|
* - Only same-document `GET` requests are redacted. Mutation responses
|
|
@@ -23,20 +22,18 @@ import { agentNativePath } from "../client/api-path.js";
|
|
|
23
22
|
* typed isn't echoed back as fake data mid-demo.
|
|
24
23
|
* - Only `application/json` 2xx bodies. Streams (`text/event-stream`),
|
|
25
24
|
* HTML, and binary are skipped by content-type.
|
|
26
|
-
* - Framework infra endpoints (poll
|
|
27
|
-
* itself) are skipped — no PII and avoids self-recursion.
|
|
25
|
+
* - Framework infra endpoints (poll and events) are skipped.
|
|
28
26
|
* - Any error during interception falls back to the original response.
|
|
29
27
|
*/
|
|
28
|
+
import { getBrowserDemoModeEnabled } from "./browser-state.js";
|
|
30
29
|
import { redactDemoData } from "./redact.js";
|
|
31
30
|
|
|
32
|
-
const STATUS_PATH = agentNativePath("/_agent-native/demo/status");
|
|
33
31
|
const SKIP_SUBSTRINGS = [
|
|
34
|
-
"/_agent-native/demo/status",
|
|
35
32
|
"/_agent-native/poll",
|
|
36
33
|
"/_agent-native/events",
|
|
37
|
-
// Never touch agent transport. The agent
|
|
38
|
-
//
|
|
39
|
-
//
|
|
34
|
+
// Never touch agent transport. The agent receives real tool results; faking
|
|
35
|
+
// its own transcript adds no demo value and must stay clear of the
|
|
36
|
+
// tool_use/tool_result protocol. Covers
|
|
40
37
|
// "/_agent-native/agent" (stream) and "/_agent-native/agent-chat"
|
|
41
38
|
// (thread history) and any sub-paths.
|
|
42
39
|
"/_agent-native/agent",
|
|
@@ -73,16 +70,6 @@ export function shouldSkipDemoResponseRedaction(url: string): boolean {
|
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
let installed = false;
|
|
76
|
-
let demoEnabled = false;
|
|
77
|
-
let originalFetch: typeof fetch | null = null;
|
|
78
|
-
let refreshPromise: Promise<void> | null = null;
|
|
79
|
-
|
|
80
|
-
// Set once the first demo-status check completes. We DO NOT block requests on
|
|
81
|
-
// it — if status isn't known yet a response is simply passed through
|
|
82
|
-
// un-redacted (a brief first-paint window) rather than delaying transport.
|
|
83
|
-
// Injecting latency into early GETs previously risked the agent's streaming
|
|
84
|
-
// reconnect logic; transport safety wins over redacting the first paint.
|
|
85
|
-
let firstStatusDone = false;
|
|
86
73
|
|
|
87
74
|
/** Reject after `ms` so a misclassified streaming body can never hang. */
|
|
88
75
|
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
@@ -114,49 +101,17 @@ function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
|
|
|
114
101
|
return m.toUpperCase();
|
|
115
102
|
}
|
|
116
103
|
|
|
117
|
-
async function refreshDemoFlag(): Promise<void> {
|
|
118
|
-
const f = originalFetch ?? fetch;
|
|
119
|
-
try {
|
|
120
|
-
const res = await f(STATUS_PATH, { credentials: "same-origin" });
|
|
121
|
-
if (!res.ok) return;
|
|
122
|
-
const json = (await res.json()) as {
|
|
123
|
-
enabled?: boolean;
|
|
124
|
-
forced?: boolean;
|
|
125
|
-
} | null;
|
|
126
|
-
demoEnabled = json?.enabled === true || json?.forced === true;
|
|
127
|
-
} catch {
|
|
128
|
-
// Status endpoint unreachable — leave the last known value.
|
|
129
|
-
} finally {
|
|
130
|
-
firstStatusDone = true;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Refresh demo mode after the shared DB-sync stream reports a real change. */
|
|
135
|
-
export function refreshDemoModeFetchInterceptor(): Promise<void> {
|
|
136
|
-
if (!installed) return Promise.resolve();
|
|
137
|
-
if (!refreshPromise) {
|
|
138
|
-
refreshPromise = refreshDemoFlag().finally(() => {
|
|
139
|
-
refreshPromise = null;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return refreshPromise;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
104
|
/**
|
|
146
|
-
* Install the demo-mode fetch interceptor
|
|
147
|
-
* changes are driven by the shared DB-sync transport instead of a separate
|
|
148
|
-
* fixed polling loop.
|
|
105
|
+
* Install the browser-local demo-mode fetch interceptor.
|
|
149
106
|
* Idempotent and browser-only — safe to call from any hook that runs in
|
|
150
|
-
* every template root (we call it from `useDbSync`).
|
|
151
|
-
* mode is actually on.
|
|
107
|
+
* every template root (we call it from `useDbSync`).
|
|
152
108
|
*/
|
|
153
109
|
export function ensureDemoModeFetchInterceptor(): void {
|
|
154
110
|
if (typeof window === "undefined") return;
|
|
155
111
|
if (installed) return;
|
|
156
112
|
installed = true;
|
|
157
113
|
|
|
158
|
-
|
|
159
|
-
const base = originalFetch;
|
|
114
|
+
const base = window.fetch.bind(window);
|
|
160
115
|
|
|
161
116
|
window.fetch = async function patchedFetch(
|
|
162
117
|
input: RequestInfo | URL,
|
|
@@ -164,11 +119,11 @@ export function ensureDemoModeFetchInterceptor(): void {
|
|
|
164
119
|
): Promise<Response> {
|
|
165
120
|
const res = await base(input, init);
|
|
166
121
|
|
|
167
|
-
// Fast path: anything that isn't a demo
|
|
122
|
+
// Fast path: anything that isn't a browser-local-demo, plain GET returns the
|
|
168
123
|
// ORIGINAL response with zero body work and zero extra awaits — when
|
|
169
124
|
// demo mode is off this wrapper is byte-for-byte native fetch, so it
|
|
170
125
|
// cannot influence agent/run/stream transport.
|
|
171
|
-
if (!
|
|
126
|
+
if (!getBrowserDemoModeEnabled()) return res;
|
|
172
127
|
if (methodOf(input, init) !== "GET") return res;
|
|
173
128
|
if (!res.ok) return res;
|
|
174
129
|
|
|
@@ -217,6 +172,4 @@ export function ensureDemoModeFetchInterceptor(): void {
|
|
|
217
172
|
return res;
|
|
218
173
|
}
|
|
219
174
|
};
|
|
220
|
-
|
|
221
|
-
void refreshDemoModeFetchInterceptor();
|
|
222
175
|
}
|
|
@@ -416,7 +416,7 @@ function writeCloudflareRoutingManifest(distDir: string, apps: string[]): void {
|
|
|
416
416
|
const dispatch = apps
|
|
417
417
|
.map(
|
|
418
418
|
(a) =>
|
|
419
|
-
` if (pathname === "/${a}" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
|
|
419
|
+
` if (pathname === "/${a}" || pathname === "/${a}.data" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
|
|
420
420
|
)
|
|
421
421
|
.join("\n");
|
|
422
422
|
const dispatchRootFrameworkRoutes = apps.includes("dispatch")
|
|
@@ -880,7 +880,7 @@ function patchNetlifyFunctionEntry(
|
|
|
880
880
|
const pathConfig =
|
|
881
881
|
app === "dispatch"
|
|
882
882
|
? ["/_agent-native/*", "/.well-known/*", `${basePath}/*`]
|
|
883
|
-
: [basePath, `${basePath}/*`];
|
|
883
|
+
: [basePath, `${basePath}.data`, `${basePath}/*`];
|
|
884
884
|
const normalizeBasePathHelper =
|
|
885
885
|
app === "dispatch"
|
|
886
886
|
? ""
|
|
@@ -599,7 +599,6 @@ export async function mergeCoreSharingActions(
|
|
|
599
599
|
"change-appearance",
|
|
600
600
|
() => import("../appearance/actions/change-appearance.js"),
|
|
601
601
|
],
|
|
602
|
-
["toggle-demo-mode", () => import("../demo/actions/toggle-demo-mode.js")],
|
|
603
602
|
// Audit log — read surface (who changed what, when, agent vs human).
|
|
604
603
|
[
|
|
605
604
|
"list-audit-events",
|
|
@@ -1205,36 +1205,6 @@ export function createCoreRoutesPlugin(
|
|
|
1205
1205
|
}),
|
|
1206
1206
|
);
|
|
1207
1207
|
|
|
1208
|
-
// Demo-mode status — read by the client fetch interceptor and the
|
|
1209
|
-
// Demo mode settings toggle. `forced` reflects the DEMO_MODE env (a
|
|
1210
|
-
// hosted demo deployment); `enabled` ORs that with the per-user
|
|
1211
|
-
// application_state toggle. No request-context dependency: the env case
|
|
1212
|
-
// needs nothing, and the per-user case resolves the session straight
|
|
1213
|
-
// off the request cookie via getSession(event).
|
|
1214
|
-
getH3App(nitroApp).use(
|
|
1215
|
-
`${P}/demo/status`,
|
|
1216
|
-
defineEventHandler(async (event) => {
|
|
1217
|
-
const { isDemoModeForced } = await import("../demo/config.js");
|
|
1218
|
-
const forced = isDemoModeForced();
|
|
1219
|
-
if (forced) return { enabled: true, forced: true };
|
|
1220
|
-
try {
|
|
1221
|
-
const session = await getSession(event);
|
|
1222
|
-
const email = session?.email;
|
|
1223
|
-
if (!email) return { enabled: false, forced: false };
|
|
1224
|
-
const { appStateGet } =
|
|
1225
|
-
await import("../application-state/store.js");
|
|
1226
|
-
const state = await appStateGet(email, "demo-mode");
|
|
1227
|
-
return {
|
|
1228
|
-
enabled:
|
|
1229
|
-
(state as { enabled?: boolean } | null)?.enabled === true,
|
|
1230
|
-
forced: false,
|
|
1231
|
-
};
|
|
1232
|
-
} catch {
|
|
1233
|
-
return { enabled: false, forced: false };
|
|
1234
|
-
}
|
|
1235
|
-
}),
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1238
1208
|
// Polling
|
|
1239
1209
|
getH3App(nitroApp).use(`${P}/poll`, createPollHandler());
|
|
1240
1210
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: native-navigation
|
|
3
|
+
description: >-
|
|
4
|
+
Use when adding or reviewing UI navigation so internal routes preserve
|
|
5
|
+
normal SPA behavior plus Cmd/Ctrl-click and middle-click new-tab semantics.
|
|
6
|
+
scope: dev
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Native Navigation
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
Pure navigation must render a real React Router `<Link>` for same-SPA routes,
|
|
14
|
+
a native `<a href>` for external/public or explicit new-tab destinations, or a
|
|
15
|
+
shadcn `Button asChild` wrapper around one. Do not use a button whose only job
|
|
16
|
+
is to call `navigate()`, `window.open()`, or `window.location`.
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Browser modifier-click, middle-click, keyboard activation, accessibility, and
|
|
21
|
+
normal client-side routing come from real links. Imperative JavaScript
|
|
22
|
+
navigation hides the destination and blocks those browser semantics.
|
|
23
|
+
|
|
24
|
+
## How
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Link } from "react-router";
|
|
28
|
+
|
|
29
|
+
<Link to={`/items/${item.id}`}>...</Link>
|
|
30
|
+
|
|
31
|
+
<Button asChild>
|
|
32
|
+
<Link to="/settings">Settings</Link>
|
|
33
|
+
</Button>
|
|
34
|
+
|
|
35
|
+
<a href={previewUrl} target="_blank" rel="noopener noreferrer">
|
|
36
|
+
Preview
|
|
37
|
+
</a>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Use `Link` for same-SPA internal routes.
|
|
41
|
+
- Use `<a>` when the URL is external, crosses an app/public boundary, is an
|
|
42
|
+
auth or sign-in destination, or intentionally opens in a new tab.
|
|
43
|
+
- Preserve query strings and hashes in `to` or `href`.
|
|
44
|
+
- For cards with nested actions, link only the non-interactive card region or
|
|
45
|
+
use a transparent Link overlay; never nest links or buttons.
|
|
46
|
+
- If selection mode changes click behavior, prevent default only for an
|
|
47
|
+
unmodified primary click and preserve modified clicks.
|
|
48
|
+
|
|
49
|
+
Keep imperative navigation for post-mutation redirects, OAuth and popup flows,
|
|
50
|
+
downloads, media/blob URLs, playback, keyboard shortcuts, and stateful tabs.
|
|
51
|
+
|
|
52
|
+
## Review
|
|
53
|
+
|
|
54
|
+
Search for `onClick={() => navigate`, `window.open`, `window.location`,
|
|
55
|
+
`onAuxClick`, and buttons whose only purpose is routing. For each candidate,
|
|
56
|
+
confirm whether it is pure navigation or a stateful workflow before changing
|
|
57
|
+
it.
|
|
58
|
+
|
|
59
|
+
Verify the rendered DOM exposes an `href`, then test normal click, Cmd/Ctrl-click,
|
|
60
|
+
middle-click, keyboard Enter, and nested controls where applicable. Run oxfmt
|
|
61
|
+
and focused app tests or typechecks for the touched package.
|
|
62
|
+
|
|
63
|
+
## Related Skills
|
|
64
|
+
|
|
65
|
+
- `client-side-routing`
|
|
66
|
+
- `frontend-design`
|
|
67
|
+
- `shadcn-ui`
|
|
@@ -58,6 +58,23 @@ once — do not switch to db-patch or raw SQL.
|
|
|
58
58
|
|
|
59
59
|
Do not use `app-db` as a dashboard source. For first-party events collected through `/track`, use `source: "first-party"` or the `query-agent-native-analytics` action rather than raw internal `db-query`.
|
|
60
60
|
|
|
61
|
+
AI-generated first-party panels are dashboard-time-bound by default. Set
|
|
62
|
+
`config.timeScope` to `"dashboard"` and include the matching dashboard time
|
|
63
|
+
filter in the SQL. The allowed values are:
|
|
64
|
+
|
|
65
|
+
- `dashboard`: use the dashboard-selected time range; the default for ordinary metrics.
|
|
66
|
+
- `fixed-window`: use an explicit bounded window independent of the dashboard filter.
|
|
67
|
+
- `cohort-history`: use the bounded history of an explicitly defined cohort.
|
|
68
|
+
- `all-time`: scan all available history; use only when the user requests it and
|
|
69
|
+
put `all-time`, `lifetime`, or `historical` in the title or description.
|
|
70
|
+
|
|
71
|
+
`{{timeRange}}` requires an explicit matching `filters` entry with
|
|
72
|
+
`id: "timeRange"` and `type: "select"`. `{{<id>Start}}` and `{{<id>End}}`
|
|
73
|
+
require a matching `filters` entry with that id and `type: "date-range"`.
|
|
74
|
+
Do not rely on undeclared time variables. Server validation rejects unbound
|
|
75
|
+
first-party SQL, so declare the filter or choose an explicit non-dashboard
|
|
76
|
+
scope before saving.
|
|
77
|
+
|
|
61
78
|
## Creating A Dashboard
|
|
62
79
|
|
|
63
80
|
When the user asks for a dashboard:
|
|
@@ -154,7 +171,8 @@ Notes:
|
|
|
154
171
|
"source": "first-party",
|
|
155
172
|
"chartType": "metric",
|
|
156
173
|
"width": 1,
|
|
157
|
-
"
|
|
174
|
+
"config": { "timeScope": "dashboard" },
|
|
175
|
+
"sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'click' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
|
|
158
176
|
},
|
|
159
177
|
{
|
|
160
178
|
"id": "kpi-signups",
|
|
@@ -162,7 +180,8 @@ Notes:
|
|
|
162
180
|
"source": "first-party",
|
|
163
181
|
"chartType": "metric",
|
|
164
182
|
"width": 1,
|
|
165
|
-
"
|
|
183
|
+
"config": { "timeScope": "dashboard" },
|
|
184
|
+
"sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'signup' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
|
|
166
185
|
},
|
|
167
186
|
{
|
|
168
187
|
"id": "kpi-active",
|
|
@@ -170,7 +189,8 @@ Notes:
|
|
|
170
189
|
"source": "first-party",
|
|
171
190
|
"chartType": "metric",
|
|
172
191
|
"width": 1,
|
|
173
|
-
"
|
|
192
|
+
"config": { "timeScope": "dashboard" },
|
|
193
|
+
"sql": "SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
|
|
174
194
|
},
|
|
175
195
|
// Section header switches the grid to 2 columns for the panels below it.
|
|
176
196
|
{
|
|
@@ -186,7 +206,8 @@ Notes:
|
|
|
186
206
|
"source": "first-party",
|
|
187
207
|
"chartType": "line",
|
|
188
208
|
"width": 2,
|
|
189
|
-
"
|
|
209
|
+
"config": { "timeScope": "dashboard" },
|
|
210
|
+
"sql": "SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY 1 ORDER BY 1",
|
|
190
211
|
},
|
|
191
212
|
],
|
|
192
213
|
}
|
|
@@ -196,6 +217,13 @@ Notes:
|
|
|
196
217
|
|
|
197
218
|
`filters[]` defines dashboard-wide controls. Filter values are available in panel SQL through `{{var}}` interpolation. Date ranges emit `{{<id>Start}}` and `{{<id>End}}`.
|
|
198
219
|
|
|
220
|
+
For dashboard-time-bound first-party SQL, use `config.timeScope: "dashboard"`
|
|
221
|
+
and a predicate that consumes the declared filter, such as
|
|
222
|
+
`event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'`. A
|
|
223
|
+
`{{timeRange}}` token must have a matching select filter and SQL branches for
|
|
224
|
+
its options; date variables must have a matching date-range filter. The server
|
|
225
|
+
rejects unbound first-party SQL during dashboard validation.
|
|
226
|
+
|
|
199
227
|
**Filter ids must be unique.** Two filters with the same `id` collide on the same URL param, so changing one visibly updates the other in the UI. The dashboard save endpoint rejects duplicates with a 400.
|
|
200
228
|
|
|
201
229
|
**Use `type: "date-range"` for paired start/end dates.** Don't add two `type: "date"` filters labeled "Start" and "End" — even with distinct ids, that ships the wrong UX. A single date-range filter renders as the "from … to …" pair and exposes both halves to SQL via `{{<id>Start}}` / `{{<id>End}}`.
|
|
@@ -244,6 +272,17 @@ type DashboardPatch = {
|
|
|
244
272
|
variables?: Record<string, string>;
|
|
245
273
|
};
|
|
246
274
|
|
|
275
|
+
type PanelTimeScope =
|
|
276
|
+
| "dashboard"
|
|
277
|
+
| "fixed-window"
|
|
278
|
+
| "cohort-history"
|
|
279
|
+
| "all-time";
|
|
280
|
+
|
|
281
|
+
type PanelConfig = Record<string, unknown> & {
|
|
282
|
+
// Use "dashboard" for AI-generated first-party panels by default.
|
|
283
|
+
timeScope?: PanelTimeScope;
|
|
284
|
+
};
|
|
285
|
+
|
|
247
286
|
type PanelPatch = {
|
|
248
287
|
title?: string;
|
|
249
288
|
sql?: string;
|
|
@@ -268,7 +307,7 @@ type PanelPatch = {
|
|
|
268
307
|
width?: number;
|
|
269
308
|
columns?: number;
|
|
270
309
|
tab?: string;
|
|
271
|
-
config?:
|
|
310
|
+
config?: PanelConfig;
|
|
272
311
|
description?: string;
|
|
273
312
|
};
|
|
274
313
|
|
|
@@ -346,7 +385,9 @@ dashboard
|
|
|
346
385
|
source: "first-party",
|
|
347
386
|
chartType: "metric",
|
|
348
387
|
width: 1,
|
|
349
|
-
|
|
388
|
+
config: { timeScope: "dashboard" },
|
|
389
|
+
// Assumes filters includes { id: "date", type: "date-range", default: "30d" }.
|
|
390
|
+
sql: "SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
|
|
350
391
|
})
|
|
351
392
|
.atTop();
|
|
352
393
|
```
|
|
@@ -441,7 +482,7 @@ For a **first-party analytics** dashboard, prefer `compose-dashboard` over hand-
|
|
|
441
482
|
- **Never hand-author large first-party configs panel-by-panel.** Call `compose-dashboard` with the metric keys instead.
|
|
442
483
|
- Unknown metric keys are skipped and reported in `unknownMetrics` (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in `invalidMetrics`.
|
|
443
484
|
- By default (no `overwrite`), composing into an existing dashboard APPENDS the new panels and skips ids already present. `overwrite: true` replaces the whole config.
|
|
444
|
-
- Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides.
|
|
485
|
+
- Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides. Request `'all'` only when the user asks for all-time coverage, and describe it as full available history.
|
|
445
486
|
- Returns `{ dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `panelCount` as proof-of-done.
|
|
446
487
|
|
|
447
488
|
Available metric keys: `total-signups`, `signups-over-time`, `signups-by-template`, `sessions-by-app`, `sessions-over-time`, `replay-sessions`, `replay-chunks-over-time`, `recent-replay-sessions`, `signed-in-vs-anon`, `total-template-clicks`, `total-demo-clicks`, `total-cli-copies`, `template-interest-over-time`, `clicks-by-template`, `demo-clicks-by-template`, `cli-copies-by-template`, `cli-copies-over-time`, `pageviews-over-time`, `top-referrer-domains`, `referred-signups-30d`, `viral-signup-share-30d`, `clip-share-signups-30d`, `signups-by-referral-source`, `referred-signups-over-time`, `top-referrers`, `share-funnel-30d`, `viral-participation-rate-90d`, `viral-coefficient-90d`, `activated-referrers-90d`.
|