@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
|
@@ -249,32 +249,40 @@
|
|
|
249
249
|
The recorded visitor cursor is a core session-replay affordance: users need
|
|
250
250
|
to see the Mac-style arrow where the visitor clicked, including on dark
|
|
251
251
|
recordings. rrweb's bundled cursor is a small black sprite that disappears
|
|
252
|
-
against those pages.
|
|
253
|
-
|
|
252
|
+
against those pages. Use the normal black Mac cursor with its white outline;
|
|
253
|
+
the small shadow below keeps it legible on either background. */
|
|
254
254
|
.an-replay-stage-root .replayer-mouse {
|
|
255
255
|
position: absolute;
|
|
256
256
|
z-index: 2;
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
/* Render at 2x locally, then downscale to the compact 0.72rem x 0.96rem
|
|
258
|
+
screen size. This keeps the small cursor's rounded outline smooth. */
|
|
259
|
+
width: 1.44rem;
|
|
260
|
+
height: 1.92rem;
|
|
259
261
|
border-radius: 0;
|
|
260
262
|
background: transparent;
|
|
261
263
|
background-image: none;
|
|
262
264
|
box-shadow: none;
|
|
263
|
-
|
|
265
|
+
visibility: hidden;
|
|
266
|
+
/* The stage is fit-to-container, but the recorded cursor should stay at a
|
|
267
|
+
normal on-screen size. The stage supplies the inverse scale variable. */
|
|
268
|
+
transform: scale(calc(var(--an-replay-cursor-scale, 1) * 0.5));
|
|
269
|
+
transform-origin: top left;
|
|
264
270
|
transition:
|
|
265
271
|
left 80ms linear,
|
|
266
272
|
top 80ms linear;
|
|
267
273
|
pointer-events: none;
|
|
268
274
|
}
|
|
269
275
|
|
|
276
|
+
.an-replay-stage-root .replayer-mouse.has-position {
|
|
277
|
+
visibility: visible;
|
|
278
|
+
}
|
|
279
|
+
|
|
270
280
|
.an-replay-stage-root .replayer-mouse::before {
|
|
271
281
|
content: "";
|
|
272
282
|
position: absolute;
|
|
273
283
|
inset: 0;
|
|
274
|
-
background:
|
|
275
|
-
|
|
276
|
-
filter: drop-shadow(0 0 1px hsl(0 0% 0% / 0.95))
|
|
277
|
-
drop-shadow(0 1px 3px hsl(0 0% 0% / 0.35));
|
|
284
|
+
background: url("./assets/replay-cursor.svg") center / contain no-repeat;
|
|
285
|
+
filter: drop-shadow(0 1px 1px hsl(0 0% 0% / 0.4));
|
|
278
286
|
}
|
|
279
287
|
|
|
280
288
|
.an-replay-stage-root .replayer-mouse::after {
|
|
@@ -41,6 +41,12 @@ function daysAgo(n: number): string {
|
|
|
41
41
|
return d.toISOString().slice(0, 10);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Keep the legacy "all" date-range sentinel out of provider queries. Analytics
|
|
45
|
+
// data cannot predate the Unix epoch, so this is equivalent to an unbounded
|
|
46
|
+
// lower date while remaining valid for BigQuery DATE/TIMESTAMP expressions.
|
|
47
|
+
const ALL_TIME_START = "1970-01-01";
|
|
48
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
49
|
+
|
|
44
50
|
/** Date-valued filters whose default may use the "Nd" / "today" shorthand. */
|
|
45
51
|
const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
|
|
46
52
|
"date",
|
|
@@ -70,6 +76,18 @@ function resolveDefault(raw: string | undefined, type: FilterType): string {
|
|
|
70
76
|
return raw;
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
function resolveDateValue(
|
|
80
|
+
raw: string | undefined,
|
|
81
|
+
allTimeValue: string,
|
|
82
|
+
): string {
|
|
83
|
+
const value = raw?.trim();
|
|
84
|
+
if (!value) return "";
|
|
85
|
+
if (value.toLowerCase() === "all") return allTimeValue;
|
|
86
|
+
|
|
87
|
+
const resolved = resolveDefault(value, "date");
|
|
88
|
+
return ISO_DATE_RE.test(resolved) ? resolved : "";
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
export function resolveFilterVars(
|
|
74
92
|
filters: DashboardFilter[],
|
|
75
93
|
getParam: (key: string) => string,
|
|
@@ -79,17 +97,27 @@ export function resolveFilterVars(
|
|
|
79
97
|
if (f.type === "date-range") {
|
|
80
98
|
const startKey = `${f.id}Start`;
|
|
81
99
|
const endKey = `${f.id}End`;
|
|
82
|
-
out[startKey] =
|
|
83
|
-
|
|
100
|
+
out[startKey] =
|
|
101
|
+
resolveDateValue(getParam(startKey), ALL_TIME_START) ||
|
|
102
|
+
resolveDateValue(resolveDefault(f.default, f.type), ALL_TIME_START);
|
|
103
|
+
out[endKey] =
|
|
104
|
+
resolveDateValue(getParam(endKey), daysAgo(0)) || daysAgo(0);
|
|
84
105
|
} else if (f.type === "toggle" || f.type === "toggle-date") {
|
|
85
106
|
// Toggles have no "off value" default — if the user hasn't opted in
|
|
86
107
|
// via the URL, the SQL-side conditional block ({{?id}}...{{/id}})
|
|
87
108
|
// must see an empty value so it doesn't emit. Otherwise the filter
|
|
88
109
|
// looks "off" in the UI but still filters the data.
|
|
89
|
-
out[f.id] =
|
|
110
|
+
out[f.id] =
|
|
111
|
+
f.type === "toggle-date"
|
|
112
|
+
? resolveDateValue(getParam(f.id), ALL_TIME_START)
|
|
113
|
+
: getParam(f.id);
|
|
90
114
|
} else {
|
|
91
115
|
const v = getParam(f.id);
|
|
92
|
-
out[f.id] =
|
|
116
|
+
out[f.id] =
|
|
117
|
+
f.type === "date"
|
|
118
|
+
? resolveDateValue(v, ALL_TIME_START) ||
|
|
119
|
+
resolveDateValue(resolveDefault(f.default, f.type), ALL_TIME_START)
|
|
120
|
+
: v || resolveDefault(f.default, f.type);
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
123
|
return out;
|
|
@@ -297,7 +297,10 @@ const PanelCell = memo(function PanelCell({
|
|
|
297
297
|
[panel, vars],
|
|
298
298
|
);
|
|
299
299
|
const resolvedSql = useMemo(
|
|
300
|
-
() =>
|
|
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[];
|
|
@@ -102,7 +102,7 @@ export function IssueDetail({
|
|
|
102
102
|
{statusLabel(issue.status)}
|
|
103
103
|
</span>
|
|
104
104
|
</div>
|
|
105
|
-
<h2 className="
|
|
105
|
+
<h2 className="break-words text-lg font-semibold text-foreground">
|
|
106
106
|
{issue.title}
|
|
107
107
|
</h2>
|
|
108
108
|
{issue.culprit ? (
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "@tabler/icons-react";
|
|
35
35
|
import { useQuery } from "@tanstack/react-query";
|
|
36
36
|
import {
|
|
37
|
+
type CSSProperties,
|
|
37
38
|
type ReactNode,
|
|
38
39
|
useCallback,
|
|
39
40
|
useEffect,
|
|
@@ -46,6 +47,13 @@ import { Link, useParams } from "react-router";
|
|
|
46
47
|
|
|
47
48
|
import { Button } from "@/components/ui/button";
|
|
48
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";
|
|
49
57
|
import { Input } from "@/components/ui/input";
|
|
50
58
|
import {
|
|
51
59
|
Popover,
|
|
@@ -711,6 +719,7 @@ function ReplayPlayer({
|
|
|
711
719
|
if (!stageRootRef.current) return;
|
|
712
720
|
let cancelled = false;
|
|
713
721
|
let localReplayer: any = null;
|
|
722
|
+
let stopCursorVisibilityObserver = () => {};
|
|
714
723
|
|
|
715
724
|
async function loadReplay() {
|
|
716
725
|
const replayEvents = eventsRef.current;
|
|
@@ -761,6 +770,8 @@ function ReplayPlayer({
|
|
|
761
770
|
// Do not mutate recorded URLs/CSS; suppress viewer-page referrer leakage
|
|
762
771
|
// at the iframe boundary while retaining historical visual resources.
|
|
763
772
|
localReplayer.iframe?.setAttribute?.("referrerpolicy", "no-referrer");
|
|
773
|
+
stopCursorVisibilityObserver =
|
|
774
|
+
hideReplayCursorUntilPosition(localReplayer);
|
|
764
775
|
replayerRef.current = localReplayer;
|
|
765
776
|
const meta = localReplayer.getMetaData?.();
|
|
766
777
|
const total = Number(meta?.totalTime ?? replayDuration(replayEvents));
|
|
@@ -827,6 +838,7 @@ function ReplayPlayer({
|
|
|
827
838
|
|
|
828
839
|
return () => {
|
|
829
840
|
cancelled = true;
|
|
841
|
+
stopCursorVisibilityObserver();
|
|
830
842
|
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
831
843
|
rafRef.current = null;
|
|
832
844
|
try {
|
|
@@ -977,22 +989,26 @@ function ReplayPlayer({
|
|
|
977
989
|
<div
|
|
978
990
|
ref={stageRootRef}
|
|
979
991
|
className="an-replay-stage-root absolute left-1/2 top-1/2"
|
|
980
|
-
style={
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
992
|
+
style={
|
|
993
|
+
{
|
|
994
|
+
width: playerWidth,
|
|
995
|
+
height: playerHeight,
|
|
996
|
+
"--an-replay-cursor-scale": String(1 / fitScale),
|
|
997
|
+
transform: `translate(-50%, -50%) scale(${fitScale})`,
|
|
998
|
+
transformOrigin: "center center",
|
|
999
|
+
} as CSSProperties
|
|
1000
|
+
}
|
|
986
1001
|
/>
|
|
987
1002
|
<button
|
|
988
1003
|
type="button"
|
|
989
1004
|
className={cn(
|
|
990
1005
|
"absolute inset-0 z-20 rounded-[inherit] border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-default",
|
|
991
|
-
// INTENTIONAL —
|
|
992
|
-
// The recorded cursor is
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
1006
|
+
// INTENTIONAL — KEEP THE VIEWER CURSOR VISIBLE.
|
|
1007
|
+
// The recorded cursor is a separate overlay, while this
|
|
1008
|
+
// button owns the live hover target for pause/play. Do
|
|
1009
|
+
// not add `cursor-none`: it makes the user's cursor
|
|
1010
|
+
// disappear when they move over the preview.
|
|
1011
|
+
"cursor-pointer",
|
|
996
1012
|
)}
|
|
997
1013
|
disabled={disabled}
|
|
998
1014
|
aria-label={
|
|
@@ -1078,22 +1094,34 @@ function ReplayPlayer({
|
|
|
1078
1094
|
{formatClock(totalTime)}
|
|
1079
1095
|
</span>
|
|
1080
1096
|
|
|
1081
|
-
<
|
|
1082
|
-
|
|
1097
|
+
<DropdownMenu>
|
|
1098
|
+
<DropdownMenuTrigger asChild>
|
|
1083
1099
|
<button
|
|
1084
|
-
key={option}
|
|
1085
1100
|
type="button"
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
"bg-background text-foreground shadow-sm",
|
|
1090
|
-
)}
|
|
1091
|
-
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"
|
|
1092
1104
|
>
|
|
1093
|
-
{
|
|
1105
|
+
{speed}x
|
|
1094
1106
|
</button>
|
|
1095
|
-
|
|
1096
|
-
|
|
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>
|
|
1097
1125
|
|
|
1098
1126
|
<Tooltip>
|
|
1099
1127
|
<TooltipTrigger asChild>
|
|
@@ -2278,6 +2306,32 @@ function hasPlayableReplayEvents(events: unknown[]): boolean {
|
|
|
2278
2306
|
return false;
|
|
2279
2307
|
}
|
|
2280
2308
|
|
|
2309
|
+
function hideReplayCursorUntilPosition(replayer: any): () => void {
|
|
2310
|
+
const cursor = replayer?.mouse as HTMLElement | undefined;
|
|
2311
|
+
if (!cursor || typeof MutationObserver === "undefined") return () => {};
|
|
2312
|
+
|
|
2313
|
+
let observer: MutationObserver | null = null;
|
|
2314
|
+
const revealWhenPositioned = () => {
|
|
2315
|
+
if (!cursor.style.left || !cursor.style.top) return;
|
|
2316
|
+
cursor.classList.add("has-position");
|
|
2317
|
+
observer?.disconnect();
|
|
2318
|
+
observer = null;
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
cursor.classList.remove("has-position");
|
|
2322
|
+
observer = new MutationObserver(revealWhenPositioned);
|
|
2323
|
+
observer.observe(cursor, {
|
|
2324
|
+
attributes: true,
|
|
2325
|
+
attributeFilter: ["style"],
|
|
2326
|
+
});
|
|
2327
|
+
revealWhenPositioned();
|
|
2328
|
+
|
|
2329
|
+
return () => {
|
|
2330
|
+
observer?.disconnect();
|
|
2331
|
+
observer = null;
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2281
2335
|
export function replayViewportDimensions(
|
|
2282
2336
|
events: AnyReplayEvent[],
|
|
2283
2337
|
): ReplayViewportDimensions | null {
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "@tabler/icons-react";
|
|
23
23
|
import { useQuery } from "@tanstack/react-query";
|
|
24
24
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
25
|
-
import { Link,
|
|
25
|
+
import { Link, useSearchParams } from "react-router";
|
|
26
26
|
import { toast } from "sonner";
|
|
27
27
|
|
|
28
28
|
import { Badge } from "@/components/ui/badge";
|
|
@@ -205,7 +205,6 @@ export function useDebouncedUrlFilter(
|
|
|
205
205
|
|
|
206
206
|
export default function SessionsPage() {
|
|
207
207
|
const t = useT();
|
|
208
|
-
const navigate = useNavigate();
|
|
209
208
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
210
209
|
const range = readRange(searchParams.get("range"));
|
|
211
210
|
const app = searchParams.get("app") ?? "";
|
|
@@ -389,11 +388,10 @@ export default function SessionsPage() {
|
|
|
389
388
|
recording.startedAt;
|
|
390
389
|
const deviceLabel = sessionDeviceLabel(recording);
|
|
391
390
|
return (
|
|
392
|
-
<
|
|
391
|
+
<Link
|
|
393
392
|
key={recording.id}
|
|
394
|
-
|
|
393
|
+
to={href}
|
|
395
394
|
className="analytics-session-row grid w-full gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/35 focus-visible:bg-muted/35 focus-visible:outline-none"
|
|
396
|
-
onClick={() => navigate(href)}
|
|
397
395
|
aria-label={t("sessions.watchReplay")}
|
|
398
396
|
>
|
|
399
397
|
<span className="inline-flex h-10 w-[92px] items-center justify-center gap-2 rounded-md bg-primary/10 font-medium text-primary">
|
|
@@ -446,7 +444,7 @@ export default function SessionsPage() {
|
|
|
446
444
|
</span>
|
|
447
445
|
) : null}
|
|
448
446
|
</span>
|
|
449
|
-
</
|
|
447
|
+
</Link>
|
|
450
448
|
);
|
|
451
449
|
})}
|
|
452
450
|
</div>
|
|
@@ -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
|