@agent-native/core 0.99.0 → 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 +23 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +33 -38
- package/corpus/core/src/audit/config.ts +0 -1
- package/corpus/core/src/chat-threads/store.ts +57 -41
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +1 -2
- package/corpus/core/src/client/analytics.ts +13 -0
- 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/agent-chat/action-filters-a2a.ts +13 -0
- package/corpus/core/src/server/agent-teams.ts +17 -0
- package/corpus/core/src/server/core-routes-plugin.ts +0 -30
- package/corpus/core/src/server/google-oauth.ts +6 -2
- 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/assets/replay-cursor.svg +17 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
- package/corpus/templates/analytics/app/global.css +17 -9
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +32 -4
- 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/monitoring/errors/IssueDetail.tsx +1 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +77 -23
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-07-13-all-time-dashboard-filters-no-longer-send-invalid-dates-to-b.md +6 -0
- 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-error-issue-titles-wrap-instead-of-overflowing.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-saving-a-new-dashboard-view-no-longer-fails-with-a-server-er.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-session-replay-cursors-stay-hidden-until-their-first-recorde.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-session-replay-links-now-support-cmd-click-and-middle-click-.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/dashboards-store.ts +43 -4
- package/corpus/templates/analytics/server/lib/error-capture.ts +30 -67
- 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 +6 -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/library/recording-card.tsx +27 -26
- package/corpus/templates/clips/app/components/library/space-card.tsx +4 -6
- 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/app/components/sidebar/DocumentSidebar.tsx +18 -16
- 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 +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +25 -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/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.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/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +11 -0
- package/dist/client/analytics.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/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/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- 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/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +11 -1
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +15 -1
- package/dist/server/agent-teams.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/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +6 -2
- package/dist/server/google-oauth.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 +2 -2
- 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
|
@@ -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
|
+
}
|
|
@@ -1846,12 +1846,38 @@ export async function saveDashboardView(
|
|
|
1846
1846
|
orgId: ctx.orgId ?? undefined,
|
|
1847
1847
|
});
|
|
1848
1848
|
const db = getDb() as any;
|
|
1849
|
-
|
|
1849
|
+
let id = view.id ?? nanoidFallback();
|
|
1850
|
+
let existing = false;
|
|
1850
1851
|
if (view.id) {
|
|
1852
|
+
const [existingRow] = await db
|
|
1853
|
+
.select({
|
|
1854
|
+
id: schema.dashboardViews.id,
|
|
1855
|
+
dashboardId: schema.dashboardViews.dashboardId,
|
|
1856
|
+
})
|
|
1857
|
+
.from(schema.dashboardViews)
|
|
1858
|
+
.where(eq(schema.dashboardViews.id, view.id))
|
|
1859
|
+
.limit(1);
|
|
1860
|
+
if (existingRow?.dashboardId === dashboardId) {
|
|
1861
|
+
existing = true;
|
|
1862
|
+
} else if (existingRow) {
|
|
1863
|
+
// View ids are generated from names in the browser but remain globally
|
|
1864
|
+
// primary-keyed for backwards compatibility. Avoid colliding with a
|
|
1865
|
+
// same-named view on another dashboard instead of updating that row or
|
|
1866
|
+
// returning a raw unique-constraint 500.
|
|
1867
|
+
id = nanoidFallback();
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
if (existing) {
|
|
1851
1872
|
await db
|
|
1852
1873
|
.update(schema.dashboardViews)
|
|
1853
1874
|
.set({ name: view.name, filters: JSON.stringify(view.filters) })
|
|
1854
|
-
.where(
|
|
1875
|
+
.where(
|
|
1876
|
+
and(
|
|
1877
|
+
eq(schema.dashboardViews.id, id),
|
|
1878
|
+
eq(schema.dashboardViews.dashboardId, dashboardId),
|
|
1879
|
+
),
|
|
1880
|
+
);
|
|
1855
1881
|
} else {
|
|
1856
1882
|
await db.insert(schema.dashboardViews).values({
|
|
1857
1883
|
id,
|
|
@@ -1864,7 +1890,15 @@ export async function saveDashboardView(
|
|
|
1864
1890
|
const [row] = await db
|
|
1865
1891
|
.select()
|
|
1866
1892
|
.from(schema.dashboardViews)
|
|
1867
|
-
.where(
|
|
1893
|
+
.where(
|
|
1894
|
+
and(
|
|
1895
|
+
eq(schema.dashboardViews.id, id),
|
|
1896
|
+
eq(schema.dashboardViews.dashboardId, dashboardId),
|
|
1897
|
+
),
|
|
1898
|
+
);
|
|
1899
|
+
if (!row) {
|
|
1900
|
+
throw new Error("Dashboard view was not persisted");
|
|
1901
|
+
}
|
|
1868
1902
|
const dash = await getDashboard(dashboardId, ctx);
|
|
1869
1903
|
if (dash) {
|
|
1870
1904
|
recordScopedChange(
|
|
@@ -1891,7 +1925,12 @@ export async function deleteDashboardView(
|
|
|
1891
1925
|
const db = getDb() as any;
|
|
1892
1926
|
await db
|
|
1893
1927
|
.delete(schema.dashboardViews)
|
|
1894
|
-
.where(
|
|
1928
|
+
.where(
|
|
1929
|
+
and(
|
|
1930
|
+
eq(schema.dashboardViews.id, viewId),
|
|
1931
|
+
eq(schema.dashboardViews.dashboardId, dashboardId),
|
|
1932
|
+
),
|
|
1933
|
+
);
|
|
1895
1934
|
const dash = await getDashboard(dashboardId, ctx);
|
|
1896
1935
|
if (dash) {
|
|
1897
1936
|
recordScopedChange(
|
|
@@ -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";
|
|
@@ -547,6 +546,27 @@ export interface RawExceptionInput {
|
|
|
547
546
|
breadcrumbs: unknown[];
|
|
548
547
|
}
|
|
549
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Browser request cancellation is expected during navigation and query
|
|
551
|
+
* invalidation. Keep the first-party issue store aligned with the client
|
|
552
|
+
* Sentry filter, while preserving other AbortError failures for triage.
|
|
553
|
+
*/
|
|
554
|
+
export function isBenignBrowserAbortException(
|
|
555
|
+
input: Pick<RawExceptionInput, "type" | "message">,
|
|
556
|
+
): boolean {
|
|
557
|
+
const exceptionType = input.type.trim().toLowerCase();
|
|
558
|
+
const exceptionValue = input.message.trim().toLowerCase();
|
|
559
|
+
return (
|
|
560
|
+
exceptionValue === "the user aborted a request." ||
|
|
561
|
+
exceptionValue === "signal is aborted without reason" ||
|
|
562
|
+
exceptionValue === "aborterror: the user aborted a request." ||
|
|
563
|
+
exceptionValue === "aborterror: signal is aborted without reason" ||
|
|
564
|
+
(exceptionType === "aborterror" &&
|
|
565
|
+
(exceptionValue.includes("the user aborted a request") ||
|
|
566
|
+
exceptionValue.includes("signal is aborted without reason")))
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
550
570
|
function nowIso(): string {
|
|
551
571
|
return new Date().toISOString();
|
|
552
572
|
}
|
|
@@ -930,11 +950,9 @@ export async function ingestAnalyticsExceptionEvents(
|
|
|
930
950
|
let ingested = 0;
|
|
931
951
|
for (const item of events) {
|
|
932
952
|
try {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
item.derived,
|
|
937
|
-
);
|
|
953
|
+
const raw = extractExceptionInput(item.properties);
|
|
954
|
+
if (isBenignBrowserAbortException(raw)) continue;
|
|
955
|
+
await ingestException(scope, raw, item.derived);
|
|
938
956
|
ingested += 1;
|
|
939
957
|
} catch (error) {
|
|
940
958
|
console.warn("[error-capture] Failed to ingest exception event:", error);
|
|
@@ -1080,56 +1098,6 @@ export interface ListErrorIssuesFilters {
|
|
|
1080
1098
|
limit?: number;
|
|
1081
1099
|
}
|
|
1082
1100
|
|
|
1083
|
-
export const ERROR_REPORTING_ANONYMOUS_EMAIL = "anonymous@builder.io";
|
|
1084
|
-
const ERROR_REPORTING_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
1085
|
-
|
|
1086
|
-
/**
|
|
1087
|
-
* Error reports contain emails in identity fields, messages, stacks, URLs,
|
|
1088
|
-
* tags, extra context, and breadcrumbs. Redact every string at this owning
|
|
1089
|
-
* read seam so demo mode never depends on browser interception timing.
|
|
1090
|
-
*/
|
|
1091
|
-
export function anonymizeErrorReportingEmails<T>(value: T): T {
|
|
1092
|
-
if (typeof value === "string") {
|
|
1093
|
-
return value.replace(
|
|
1094
|
-
ERROR_REPORTING_EMAIL_PATTERN,
|
|
1095
|
-
ERROR_REPORTING_ANONYMOUS_EMAIL,
|
|
1096
|
-
) as T;
|
|
1097
|
-
}
|
|
1098
|
-
if (Array.isArray(value)) {
|
|
1099
|
-
return value.map((item) => anonymizeErrorReportingEmails(item)) as T;
|
|
1100
|
-
}
|
|
1101
|
-
if (value && typeof value === "object") {
|
|
1102
|
-
if (value instanceof Date) return value;
|
|
1103
|
-
const output: Record<string, unknown> = {};
|
|
1104
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1105
|
-
output[key] = anonymizeErrorReportingEmails(entry);
|
|
1106
|
-
}
|
|
1107
|
-
return output as T;
|
|
1108
|
-
}
|
|
1109
|
-
return value;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
async function isErrorDemoModeEnabled(
|
|
1113
|
-
userEmail?: string | null,
|
|
1114
|
-
): Promise<boolean> {
|
|
1115
|
-
if (process.env.DEMO_MODE === "true") return true;
|
|
1116
|
-
if (!userEmail) return false;
|
|
1117
|
-
try {
|
|
1118
|
-
// Keep demo state tied to the caller whose issues are being read.
|
|
1119
|
-
const state = await appStateGet(userEmail, "demo-mode");
|
|
1120
|
-
return state?.enabled === true;
|
|
1121
|
-
} catch {
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
function anonymizeErrorReportingEmailsInDemoMode<T>(
|
|
1127
|
-
value: T,
|
|
1128
|
-
enabled: boolean,
|
|
1129
|
-
): T {
|
|
1130
|
-
return enabled ? anonymizeErrorReportingEmails(value) : value;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1101
|
export interface ErrorIssueSummary {
|
|
1134
1102
|
id: string;
|
|
1135
1103
|
fingerprint: string;
|
|
@@ -1202,7 +1170,6 @@ export async function listErrorIssues(
|
|
|
1202
1170
|
scope: ErrorReadScope,
|
|
1203
1171
|
filters: ListErrorIssuesFilters = {},
|
|
1204
1172
|
): Promise<ErrorIssueSummary[]> {
|
|
1205
|
-
const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
|
|
1206
1173
|
const db = getDb() as any;
|
|
1207
1174
|
const limit = Math.min(
|
|
1208
1175
|
MAX_ISSUE_LIMIT,
|
|
@@ -1293,7 +1260,7 @@ export async function listErrorIssues(
|
|
|
1293
1260
|
template: row.template ?? null,
|
|
1294
1261
|
sparkline: sparklines.get(row.id) ?? new Array(SPARKLINE_DAYS).fill(0),
|
|
1295
1262
|
}));
|
|
1296
|
-
return
|
|
1263
|
+
return issues;
|
|
1297
1264
|
}
|
|
1298
1265
|
|
|
1299
1266
|
export interface ErrorEventDetail {
|
|
@@ -1385,7 +1352,6 @@ export async function getErrorIssue(
|
|
|
1385
1352
|
issueId: string,
|
|
1386
1353
|
options: { eventsLimit?: number } = {},
|
|
1387
1354
|
): Promise<ErrorIssueDetail> {
|
|
1388
|
-
const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
|
|
1389
1355
|
const db = getDb() as any;
|
|
1390
1356
|
const [issueRow] = await db
|
|
1391
1357
|
.select()
|
|
@@ -1511,14 +1477,11 @@ export async function getErrorIssue(
|
|
|
1511
1477
|
sparkline: sparklines.get(issueId) ?? new Array(SPARKLINE_DAYS).fill(0),
|
|
1512
1478
|
};
|
|
1513
1479
|
|
|
1514
|
-
return
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
},
|
|
1520
|
-
await demoModeEnabled,
|
|
1521
|
-
);
|
|
1480
|
+
return {
|
|
1481
|
+
issue,
|
|
1482
|
+
events,
|
|
1483
|
+
sessions: Array.from(sessions.values()),
|
|
1484
|
+
};
|
|
1522
1485
|
}
|
|
1523
1486
|
|
|
1524
1487
|
export interface UpdateErrorIssueInput {
|
|
@@ -163,6 +163,7 @@ const DASHBOARD_WAU_BASE_RANGE_FILTER = dashboardLookbackTimeRangeFilter(
|
|
|
163
163
|
);
|
|
164
164
|
const DASHBOARD_EMAIL_FILTER =
|
|
165
165
|
"('{{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'))";
|
|
166
|
+
const SESSION_STATUS_FILTER = `event_name = 'session status' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
|
|
166
167
|
const SIGNED_IN_ACTIVITY_KEY_SQL = USER_KEY_SQL;
|
|
167
168
|
const SIGNED_IN_ACTIVITY_FILTER = `event_name = 'session status' AND signed_in = 'true' AND ${SIGNED_IN_ACTIVITY_KEY_SQL} IS NOT NULL`;
|
|
168
169
|
const SIGNED_IN_PRODUCT_ACTIVITY_FILTER = `${SIGNED_IN_ACTIVITY_FILTER} AND ${PRODUCT_ACTIVITY_TEMPLATE_FILTER}`;
|
|
@@ -292,7 +293,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
292
293
|
width: 2,
|
|
293
294
|
windowed: false,
|
|
294
295
|
buildSql: fixed(
|
|
295
|
-
|
|
296
|
+
`SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20`,
|
|
296
297
|
),
|
|
297
298
|
config: {
|
|
298
299
|
xKey: "app",
|
|
@@ -310,7 +311,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
310
311
|
width: 1,
|
|
311
312
|
windowed: false,
|
|
312
313
|
buildSql: fixed(
|
|
313
|
-
`SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE
|
|
314
|
+
`SELECT ${EVENT_DATE_SQL} AS date, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY ${EVENT_DATE_SQL} ORDER BY date`,
|
|
314
315
|
),
|
|
315
316
|
config: { xKey: "date", yKey: "count", color: "#10b981" },
|
|
316
317
|
},
|
|
@@ -380,7 +381,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
380
381
|
width: 1,
|
|
381
382
|
windowed: false,
|
|
382
383
|
buildSql: fixed(
|
|
383
|
-
|
|
384
|
+
`SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE ${SESSION_STATUS_FILTER} GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in`,
|
|
384
385
|
),
|
|
385
386
|
config: {
|
|
386
387
|
xKey: "signed_in",
|
|
@@ -1007,14 +1008,28 @@ export function buildPanel(
|
|
|
1007
1008
|
overrides.width <= 6
|
|
1008
1009
|
? overrides.width
|
|
1009
1010
|
: metric.width;
|
|
1011
|
+
const title =
|
|
1012
|
+
overrides.title?.trim() ||
|
|
1013
|
+
(window === "all"
|
|
1014
|
+
? metric.title.replace(/\(\d+d\)$/i, "(all-time)")
|
|
1015
|
+
: metric.title);
|
|
1016
|
+
const config = { ...metric.config };
|
|
1017
|
+
if (metric.windowed) {
|
|
1018
|
+
config.timeScope = window === "all" ? "all-time" : "fixed-window";
|
|
1019
|
+
if (window === "all") {
|
|
1020
|
+
const description =
|
|
1021
|
+
typeof config.description === "string" ? config.description.trim() : "";
|
|
1022
|
+
config.description = `${description ? `${description} ` : ""}This panel is explicitly configured for an all-time window.`;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1010
1025
|
return {
|
|
1011
1026
|
id: overrides.id?.trim() || metric.key,
|
|
1012
|
-
title
|
|
1027
|
+
title,
|
|
1013
1028
|
chartType: overrides.chartType?.trim() || metric.chartType,
|
|
1014
1029
|
source: "first-party",
|
|
1015
1030
|
width,
|
|
1016
1031
|
sql: metric.buildSql(window),
|
|
1017
1032
|
// Clone so callers can't mutate the shared catalog config object.
|
|
1018
|
-
config
|
|
1033
|
+
config,
|
|
1019
1034
|
};
|
|
1020
1035
|
}
|
|
@@ -663,7 +663,7 @@ export function resolveSessionReplayAgentAccess(
|
|
|
663
663
|
resourceId: recordingId,
|
|
664
664
|
});
|
|
665
665
|
// Replay grants are always minted by an authenticated viewer. Fail closed
|
|
666
|
-
// for grants without that signed identity:
|
|
666
|
+
// for grants without that signed identity: access policy is viewer-scoped,
|
|
667
667
|
// so guessing from the ambient request could expose identities to agents.
|
|
668
668
|
if (!result.ok || !result.viewerEmail) return null;
|
|
669
669
|
return { viewerEmail: result.viewerEmail };
|
|
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
4
4
|
|
|
5
|
-
import { appStateGet } from "@agent-native/core/application-state";
|
|
6
5
|
import {
|
|
7
6
|
deletePrivateBlob,
|
|
8
7
|
putPrivateBlob,
|
|
@@ -258,8 +257,6 @@ const DEFAULT_REPLAY_MAX_REQUESTS_PER_MINUTE = 120;
|
|
|
258
257
|
const RETENTION_DELETE_BATCH_SIZE = 500;
|
|
259
258
|
const REPLAY_PRIVATE_BLOB_REF_KIND = "agent-native.session-replay.private-blob";
|
|
260
259
|
const REPLAY_PRIVATE_BLOB_REF_VERSION = 1;
|
|
261
|
-
const SESSION_DEMO_EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
262
|
-
const SESSION_DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
|
|
263
260
|
let inlineReplayFallbackWarned = false;
|
|
264
261
|
|
|
265
262
|
function replayError(message: string, statusCode: number): Error {
|
|
@@ -297,89 +294,6 @@ function replayEmail(value: unknown): string | null {
|
|
|
297
294
|
return raw && raw.includes("@") ? raw : null;
|
|
298
295
|
}
|
|
299
296
|
|
|
300
|
-
function normalizedEmail(value: unknown): string | null {
|
|
301
|
-
return replayEmail(value)?.toLowerCase() ?? null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function isBuilderEmail(value: unknown): boolean {
|
|
305
|
-
return normalizedEmail(value)?.endsWith("@builder.io") === true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function isSessionDemoModeEnabled(
|
|
309
|
-
userEmail?: string | null,
|
|
310
|
-
): Promise<boolean> {
|
|
311
|
-
if (process.env.DEMO_MODE === "true") return true;
|
|
312
|
-
if (!userEmail) return false;
|
|
313
|
-
try {
|
|
314
|
-
// These reads also run from custom routes and actions, so use the
|
|
315
|
-
// authenticated scope explicitly instead of ambient request state.
|
|
316
|
-
const state = await appStateGet(userEmail, "demo-mode");
|
|
317
|
-
return state?.enabled === true;
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function demoBuilderSessionCondition() {
|
|
324
|
-
return or(
|
|
325
|
-
sql`lower(${schema.sessionRecordings.userId}) like ${"%@builder.io"}`,
|
|
326
|
-
sql`lower(${schema.sessionRecordings.userKey}) like ${"%@builder.io"}`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
class SessionDemoAnonymizer {
|
|
331
|
-
aliasFor(value: unknown): string | null {
|
|
332
|
-
const raw = replayString(value);
|
|
333
|
-
if (!raw || raw.match(SESSION_DEMO_EMAIL_PATTERN)?.[0] !== raw) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
return SESSION_DEMO_ANONYMOUS_EMAIL;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
summarize(row: any, role?: SessionReplayAccessRole): SessionRecordingSummary {
|
|
340
|
-
const summary = rowToSessionRecordingSummary(row, role);
|
|
341
|
-
return {
|
|
342
|
-
...summary,
|
|
343
|
-
userId: this.aliasFor(row.userId) ?? summary.userId,
|
|
344
|
-
userKey: this.aliasFor(row.userKey) ?? summary.userKey,
|
|
345
|
-
ownerEmail: this.aliasFor(summary.ownerEmail) ?? summary.ownerEmail,
|
|
346
|
-
metadata: anonymizeEmailValue(summary.metadata, this) as Record<
|
|
347
|
-
string,
|
|
348
|
-
unknown
|
|
349
|
-
>,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function anonymizeEmailValue(
|
|
355
|
-
value: unknown,
|
|
356
|
-
anonymizer: SessionDemoAnonymizer,
|
|
357
|
-
): unknown {
|
|
358
|
-
const alias = anonymizer.aliasFor(value);
|
|
359
|
-
if (alias) return alias;
|
|
360
|
-
if (Array.isArray(value)) {
|
|
361
|
-
return value.map((item) => anonymizeEmailValue(item, anonymizer));
|
|
362
|
-
}
|
|
363
|
-
if (typeof value === "string") {
|
|
364
|
-
return value.replace(SESSION_DEMO_EMAIL_PATTERN, (email) => {
|
|
365
|
-
return anonymizer.aliasFor(email) ?? email;
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
if (value && typeof value === "object") {
|
|
369
|
-
const out: Record<string, unknown> = {};
|
|
370
|
-
for (const [key, item] of Object.entries(value)) {
|
|
371
|
-
out[key] = anonymizeEmailValue(item, anonymizer);
|
|
372
|
-
}
|
|
373
|
-
return out;
|
|
374
|
-
}
|
|
375
|
-
return value;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function assertDemoVisibleSession(row: any): void {
|
|
379
|
-
if (isBuilderEmail(row.userId) || isBuilderEmail(row.userKey)) return;
|
|
380
|
-
throw replayError("Session recording not found", 404);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
297
|
function replayInteger(value: unknown): number | null {
|
|
384
298
|
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
385
299
|
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
@@ -1686,7 +1600,6 @@ export async function listSessionRecordings(
|
|
|
1686
1600
|
filters: SessionReplayListFilters = {},
|
|
1687
1601
|
): Promise<SessionRecordingSummary[]> {
|
|
1688
1602
|
const db = getDb() as any;
|
|
1689
|
-
const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
|
|
1690
1603
|
const limit = Math.min(
|
|
1691
1604
|
MAX_SESSION_RECORDINGS_LIMIT,
|
|
1692
1605
|
Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
|
|
@@ -1699,7 +1612,6 @@ export async function listSessionRecordings(
|
|
|
1699
1612
|
replayVisibleIdentityCondition(),
|
|
1700
1613
|
replayPlayableEventsCondition(),
|
|
1701
1614
|
];
|
|
1702
|
-
if (demoMode) conditions.push(demoBuilderSessionCondition());
|
|
1703
1615
|
if (filters.app)
|
|
1704
1616
|
conditions.push(eq(schema.sessionRecordings.app, filters.app));
|
|
1705
1617
|
if (filters.template) {
|
|
@@ -1753,22 +1665,13 @@ export async function listSessionRecordings(
|
|
|
1753
1665
|
.where(and(...conditions))
|
|
1754
1666
|
.orderBy(desc(schema.sessionRecordings.startedAt))
|
|
1755
1667
|
.limit(limit);
|
|
1756
|
-
|
|
1757
|
-
return rows.map((row: any) => rowToSessionRecordingSummary(row));
|
|
1758
|
-
}
|
|
1759
|
-
const anonymizer = new SessionDemoAnonymizer();
|
|
1760
|
-
return rows
|
|
1761
|
-
.filter(
|
|
1762
|
-
(row: any) => isBuilderEmail(row.userId) || isBuilderEmail(row.userKey),
|
|
1763
|
-
)
|
|
1764
|
-
.map((row: any) => anonymizer.summarize(row));
|
|
1668
|
+
return rows.map((row: any) => rowToSessionRecordingSummary(row));
|
|
1765
1669
|
}
|
|
1766
1670
|
|
|
1767
1671
|
export async function getSessionReplaySummary(
|
|
1768
1672
|
recordingId: string,
|
|
1769
1673
|
scope: SessionReplayScope,
|
|
1770
1674
|
): Promise<SessionRecordingSummary> {
|
|
1771
|
-
const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
|
|
1772
1675
|
const access = await resolveAccess("session-recording", recordingId, {
|
|
1773
1676
|
userEmail: scope.userEmail,
|
|
1774
1677
|
orgId: scope.orgId ?? undefined,
|
|
@@ -1777,22 +1680,15 @@ export async function getSessionReplaySummary(
|
|
|
1777
1680
|
if (!isVisibleSessionRecording(access.resource)) {
|
|
1778
1681
|
throw replayError("Session recording not found", 404);
|
|
1779
1682
|
}
|
|
1780
|
-
|
|
1781
|
-
return rowToSessionRecordingSummary(access.resource, access.role);
|
|
1782
|
-
}
|
|
1783
|
-
assertDemoVisibleSession(access.resource);
|
|
1784
|
-
return new SessionDemoAnonymizer().summarize(access.resource, access.role);
|
|
1683
|
+
return rowToSessionRecordingSummary(access.resource, access.role);
|
|
1785
1684
|
}
|
|
1786
1685
|
|
|
1787
1686
|
export async function getSessionReplayTokenizedSummary(
|
|
1788
1687
|
recordingId: string,
|
|
1789
1688
|
viewerEmail: string,
|
|
1790
1689
|
): Promise<SessionRecordingSummary> {
|
|
1791
|
-
// Tokenized reads often run without an authenticated request session.
|
|
1792
|
-
//
|
|
1793
|
-
// link minted while demo mode is enabled cannot reveal real identities when
|
|
1794
|
-
// it is opened in a signed-out browser or by an external agent.
|
|
1795
|
-
const demoMode = await isSessionDemoModeEnabled(viewerEmail);
|
|
1690
|
+
// Tokenized reads often run without an authenticated request session. The
|
|
1691
|
+
// viewer identity is still required by the signed, recording-scoped grant.
|
|
1796
1692
|
const db = getDb() as any;
|
|
1797
1693
|
// guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
|
|
1798
1694
|
const [row] = await db
|
|
@@ -1803,9 +1699,7 @@ export async function getSessionReplayTokenizedSummary(
|
|
|
1803
1699
|
if (!row || !isVisibleSessionRecording(row)) {
|
|
1804
1700
|
throw replayError("Session recording not found", 404);
|
|
1805
1701
|
}
|
|
1806
|
-
|
|
1807
|
-
assertDemoVisibleSession(row);
|
|
1808
|
-
return new SessionDemoAnonymizer().summarize(row, "viewer");
|
|
1702
|
+
return rowToSessionRecordingSummary(row, "viewer");
|
|
1809
1703
|
}
|
|
1810
1704
|
|
|
1811
1705
|
function parseInlineReplayEvents(inlineData: string): unknown[] {
|
|
@@ -239,6 +239,11 @@ export function realDataFinalGuard(
|
|
|
239
239
|
// actual query. One corrective turn was enough for Sonnet but caused Luna
|
|
240
240
|
// to hit the fallback before it reached the query.
|
|
241
241
|
maxRetries: 2,
|
|
242
|
+
// The first request may use the compact starter catalog. A corrective
|
|
243
|
+
// retry must be able to reach the real source action directly; otherwise
|
|
244
|
+
// some model families spend the retry on tool-search narration and hit
|
|
245
|
+
// the canned fallback without ever running a query.
|
|
246
|
+
expandToolSurface: true,
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
|
|
@@ -281,6 +286,7 @@ export default createAgentChatPlugin({
|
|
|
281
286
|
analyticsSourceGuidanceOpening() +
|
|
282
287
|
"DASHBOARD CREATION RULE — You may create dashboards, analyses, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'create a new analysis', 'add a chart for...'). Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
|
|
283
288
|
"DASHBOARD MUTATION RULE — For dashboard edits, default to `mutate-dashboard` with the typed `dashboard.*` script API so the main payload is a string and avoids native-array serialization traps. It can move panels by id, edit titles/SQL/config, insert, duplicate, remove, and patch dashboard fields in one atomic save. The script API is constrained: no variables/imports/loops/functions, only JSON-compatible arguments on documented dashboard methods. Do not count shifting `/panels/<index>` positions for ordinary dashboard edits unless the user specifically asks for low-level JSON-pointer operations. " +
|
|
289
|
+
'FIRST-PARTY DASHBOARD TIME RULE — AI-generated `source: "first-party"` panels are dashboard-time-bound by default: set `config.timeScope` to `dashboard` and include a matching dashboard time predicate. `{{timeRange}}` requires a matching `filters` entry with `id: "timeRange"` and `type: "select"`; `{{<id>Start}}`/`{{<id>End}}` require a matching `type: "date-range"` filter with that id. Allowed `timeScope` values are `dashboard`, `fixed-window`, `cohort-history`, and `all-time`; use `all-time` only when the user requests full available history and put all-time, lifetime, or historical in the title or description. Server validation rejects unbound first-party SQL. ' +
|
|
284
290
|
"DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder`, `layout.firstPanelIds`, and `layout.groups[].rows[].rowNumber/panelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
|
|
285
291
|
'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. For visible placement requests like "second row" or "next to return rates", use row-aware placement such as `dashboard.insertPanel({...}).nextTo("retention-over-time")`, `.atRow(2)`, or `dashboard.panel("panel-a").moveNextTo("panel-b")`; these keep panels in the intended rendered row and expand/rebalance that row when needed. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.groups[].rows` as proof of visible row placement, not only flat `panelOrder`. ' +
|
|
286
292
|
"Use configured data sources and actions only. Call `data-source-status` when you need to know which providers are connected, and treat provider actions as unavailable for analysis if they return missing credentials, permission, syntax, quota, or network errors. " +
|