@agent-native/core 0.79.20 → 0.79.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/deployment.md +133 -0
  4. package/corpus/core/docs/content/faq.md +1 -1
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -0
  6. package/corpus/core/docs/content/tracking.md +3 -3
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
  9. package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
  10. package/corpus/core/src/agent/engine/registry.ts +59 -11
  11. package/corpus/core/src/client/FeedbackButton.tsx +11 -1
  12. package/corpus/core/src/client/agent-engine-key.ts +57 -10
  13. package/corpus/core/src/client/analytics.ts +78 -27
  14. package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
  15. package/corpus/core/src/client/index.ts +2 -0
  16. package/corpus/core/src/client/session-replay.ts +159 -17
  17. package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
  18. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
  19. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
  20. package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
  21. package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
  22. package/corpus/core/src/server/core-routes-plugin.ts +26 -2
  23. package/corpus/core/src/styles/blocks.css +23 -7
  24. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
  25. package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +5 -10
  26. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
  29. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
  31. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
  37. package/corpus/templates/analytics/netlify.toml +4 -0
  38. package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
  39. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
  40. package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
  41. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
  42. package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
  43. package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
  44. package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  45. package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
  46. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +20 -4
  47. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -2
  48. package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-27-desktop-storage-setup-flow.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +6 -0
  52. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  53. package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
  54. package/corpus/templates/clips/desktop/package.json +1 -0
  55. package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
  56. package/corpus/templates/clips/desktop/src/app.tsx +32 -3
  57. package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
  58. package/corpus/templates/clips/desktop/src/main.tsx +2 -0
  59. package/corpus/templates/clips/desktop/src/styles.css +67 -0
  60. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
  61. package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
  62. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
  64. package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
  65. package/corpus/templates/clips/desktop/vite.config.ts +66 -0
  66. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -1
  67. package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  68. package/corpus/templates/forms/actions/export-responses.ts +3 -2
  69. package/corpus/templates/forms/actions/list-responses.ts +2 -1
  70. package/corpus/templates/forms/actions/response-insights.ts +13 -4
  71. package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
  72. package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
  73. package/corpus/templates/forms/server/lib/integrations.ts +9 -6
  74. package/corpus/templates/forms/shared/submitter-email.ts +20 -0
  75. package/corpus/templates/forms/shared/types.ts +1 -1
  76. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  78. package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  79. package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
  80. package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
  81. package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
  82. package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
  83. package/dist/agent/engine/provider-env-vars.d.ts +1 -0
  84. package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
  85. package/dist/agent/engine/provider-env-vars.js +1 -0
  86. package/dist/agent/engine/provider-env-vars.js.map +1 -1
  87. package/dist/agent/engine/registry.d.ts.map +1 -1
  88. package/dist/agent/engine/registry.js +39 -12
  89. package/dist/agent/engine/registry.js.map +1 -1
  90. package/dist/client/FeedbackButton.d.ts.map +1 -1
  91. package/dist/client/FeedbackButton.js +6 -1
  92. package/dist/client/FeedbackButton.js.map +1 -1
  93. package/dist/client/agent-engine-key.d.ts +14 -0
  94. package/dist/client/agent-engine-key.d.ts.map +1 -1
  95. package/dist/client/agent-engine-key.js +35 -10
  96. package/dist/client/agent-engine-key.js.map +1 -1
  97. package/dist/client/analytics.d.ts.map +1 -1
  98. package/dist/client/analytics.js +71 -24
  99. package/dist/client/analytics.js.map +1 -1
  100. package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
  101. package/dist/client/blocks/library/HighlightedCode.js +1 -1
  102. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  103. package/dist/client/index.d.ts +1 -1
  104. package/dist/client/index.d.ts.map +1 -1
  105. package/dist/client/index.js +1 -1
  106. package/dist/client/index.js.map +1 -1
  107. package/dist/client/session-replay.d.ts +1 -1
  108. package/dist/client/session-replay.d.ts.map +1 -1
  109. package/dist/client/session-replay.js +115 -17
  110. package/dist/client/session-replay.js.map +1 -1
  111. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  112. package/dist/client/settings/SettingsPanel.js +42 -6
  113. package/dist/client/settings/SettingsPanel.js.map +1 -1
  114. package/dist/collab/routes.d.ts +2 -2
  115. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  116. package/dist/notifications/routes.d.ts +1 -1
  117. package/dist/observability/routes.d.ts +7 -7
  118. package/dist/progress/routes.d.ts +1 -1
  119. package/dist/resources/handlers.d.ts +1 -1
  120. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  121. package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
  122. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  123. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  124. package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
  125. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  126. package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
  127. package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
  128. package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
  129. package/dist/server/agent-engine-api-key-route.d.ts +5 -5
  130. package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
  131. package/dist/server/agent-engine-api-key-route.js +71 -11
  132. package/dist/server/agent-engine-api-key-route.js.map +1 -1
  133. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  134. package/dist/server/core-routes-plugin.js +20 -3
  135. package/dist/server/core-routes-plugin.js.map +1 -1
  136. package/dist/styles/blocks.css +23 -7
  137. package/docs/content/deployment.md +133 -0
  138. package/docs/content/faq.md +1 -1
  139. package/docs/content/multi-app-workspace.md +2 -0
  140. package/docs/content/tracking.md +3 -3
  141. package/package.json +1 -1
@@ -38,7 +38,17 @@ const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
38
38
  const DEFAULT_SERVERLESS_CHROMIUM_PACK_URL =
39
39
  "https://github.com/Sparticuz/chromium/releases/download/v149.0.0/chromium-v149.0.0-pack.x64.tar";
40
40
  const DASHBOARD_REPORT_SCREENSHOT_PARAM = "reportScreenshot";
41
+ const DASHBOARD_REPORT_PANEL_LIMIT_PARAM = "reportPanelLimit";
41
42
  const DASHBOARD_REPORT_CID = "dashboard-report-snapshot";
43
+ const COMPACT_REPORT_PANEL_LIMIT = 12;
44
+ const LOCAL_SCREENSHOT_TIMEOUT_MS = 90_000;
45
+ const SERVERLESS_SCREENSHOT_TIMEOUT_MS = 25_000;
46
+ const SERVERLESS_SECOND_READY_TIMEOUT_MS = 10_000;
47
+
48
+ type DashboardScreenshotAttempt = {
49
+ label: "full" | "compact";
50
+ panelLimit?: number;
51
+ };
42
52
 
43
53
  function daysAgo(n: number): string {
44
54
  const d = new Date();
@@ -104,7 +114,7 @@ function dashboardBaseUrl(): string {
104
114
  function buildDashboardPath(
105
115
  dashboardId: string,
106
116
  filters: Record<string, string>,
107
- options?: { reportScreenshot?: boolean },
117
+ options?: { reportScreenshot?: boolean; panelLimit?: number },
108
118
  ): string {
109
119
  const url = new URL(
110
120
  `/dashboards/${encodeURIComponent(dashboardId)}`,
@@ -116,13 +126,19 @@ function buildDashboardPath(
116
126
  if (options?.reportScreenshot) {
117
127
  url.searchParams.set(DASHBOARD_REPORT_SCREENSHOT_PARAM, "1");
118
128
  }
129
+ if (options?.panelLimit && options.panelLimit > 0) {
130
+ url.searchParams.set(
131
+ DASHBOARD_REPORT_PANEL_LIMIT_PARAM,
132
+ String(Math.floor(options.panelLimit)),
133
+ );
134
+ }
119
135
  return `${url.pathname}${url.search}`;
120
136
  }
121
137
 
122
138
  function buildDashboardUrl(
123
139
  dashboardId: string,
124
140
  filters: Record<string, string>,
125
- options?: { reportScreenshot?: boolean },
141
+ options?: { reportScreenshot?: boolean; panelLimit?: number },
126
142
  ): string {
127
143
  const path = buildDashboardPath(dashboardId, filters, options);
128
144
  const url = new URL(path, `${dashboardBaseUrl()}/`);
@@ -224,7 +240,16 @@ async function launchScreenshotBrowser() {
224
240
  return playwright.launch({ headless: true });
225
241
  }
226
242
 
227
- async function waitForDashboardReportReady(page: any): Promise<void> {
243
+ function screenshotTimeoutMs(): number {
244
+ return isServerlessBrowserRuntime()
245
+ ? SERVERLESS_SCREENSHOT_TIMEOUT_MS
246
+ : LOCAL_SCREENSHOT_TIMEOUT_MS;
247
+ }
248
+
249
+ async function waitForDashboardReportReady(
250
+ page: any,
251
+ timeout: number,
252
+ ): Promise<void> {
228
253
  try {
229
254
  await page.waitForFunction(
230
255
  `(() => {
@@ -236,7 +261,7 @@ async function waitForDashboardReportReady(page: any): Promise<void> {
236
261
  return !root.querySelector("[data-dashboard-report-loading='true']");
237
262
  })()`,
238
263
  undefined,
239
- { timeout: 90_000 },
264
+ { timeout },
240
265
  );
241
266
  await page.evaluate(`(async () => {
242
267
  await document.fonts?.ready;
@@ -281,12 +306,14 @@ async function scrollDashboardForLazyRendering(page: any): Promise<void> {
281
306
  async function captureDashboardPng(
282
307
  sub: DashboardReportSubscription,
283
308
  snapshot: ReportSnapshot,
309
+ attempt: DashboardScreenshotAttempt,
284
310
  ): Promise<Buffer> {
285
311
  const targetPath = buildDashboardPath(
286
312
  snapshot.dashboardId,
287
313
  snapshot.filters,
288
314
  {
289
315
  reportScreenshot: true,
316
+ panelLimit: attempt.panelLimit,
290
317
  },
291
318
  );
292
319
  const token = signEmbedSessionToken({
@@ -302,22 +329,28 @@ async function captureDashboardPng(
302
329
 
303
330
  const browser = await launchScreenshotBrowser();
304
331
  try {
332
+ const timeout = screenshotTimeoutMs();
305
333
  const page = await browser.newPage({
306
334
  viewport: { width: 1440, height: 1800 },
307
335
  deviceScaleFactor: 1,
308
336
  });
309
- page.setDefaultTimeout(90_000);
337
+ page.setDefaultTimeout(timeout);
310
338
  await page.emulateMedia({ media: "screen" });
311
339
  await page.goto(screenshotUrl.toString(), {
312
340
  waitUntil: "domcontentloaded",
313
- timeout: 90_000,
341
+ timeout,
314
342
  });
315
343
 
316
344
  const capture = page.locator("[data-dashboard-report-capture]");
317
- await capture.waitFor({ state: "visible", timeout: 90_000 });
318
- await waitForDashboardReportReady(page);
345
+ await capture.waitFor({ state: "visible", timeout });
346
+ await waitForDashboardReportReady(page, timeout);
319
347
  await scrollDashboardForLazyRendering(page);
320
- await waitForDashboardReportReady(page);
348
+ await waitForDashboardReportReady(
349
+ page,
350
+ isServerlessBrowserRuntime()
351
+ ? SERVERLESS_SECOND_READY_TIMEOUT_MS
352
+ : timeout,
353
+ );
321
354
 
322
355
  const box = await capture.boundingBox();
323
356
  if (box) {
@@ -340,6 +373,42 @@ async function captureDashboardPng(
340
373
  }
341
374
  }
342
375
 
376
+ function errorMessage(err: unknown): string {
377
+ return err instanceof Error ? err.message : String(err);
378
+ }
379
+
380
+ async function captureDashboardPngWithFallback(
381
+ sub: DashboardReportSubscription,
382
+ snapshot: ReportSnapshot,
383
+ ): Promise<{
384
+ png: Buffer | null;
385
+ mode: "full" | "compact" | "none";
386
+ error?: string;
387
+ }> {
388
+ const attempts: DashboardScreenshotAttempt[] = [
389
+ { label: "full" },
390
+ { label: "compact", panelLimit: COMPACT_REPORT_PANEL_LIMIT },
391
+ ];
392
+ let lastError: string | undefined;
393
+
394
+ for (const attempt of attempts) {
395
+ try {
396
+ return {
397
+ png: await captureDashboardPng(sub, snapshot, attempt),
398
+ mode: attempt.label,
399
+ };
400
+ } catch (err) {
401
+ lastError = errorMessage(err);
402
+ console.error(
403
+ `[dashboard-report] ${attempt.label} screenshot failed for subscription ${sub.id}:`,
404
+ lastError,
405
+ );
406
+ }
407
+ }
408
+
409
+ return { png: null, mode: "none", error: lastError };
410
+ }
411
+
343
412
  function reportDate(snapshot: ReportSnapshot): string {
344
413
  return new Date(snapshot.generatedAt).toLocaleDateString("en-US", {
345
414
  year: "numeric",
@@ -348,13 +417,23 @@ function reportDate(snapshot: ReportSnapshot): string {
348
417
  });
349
418
  }
350
419
 
351
- function renderReportEmailHtml(snapshot: ReportSnapshot): string {
420
+ function renderReportEmailHtml(
421
+ snapshot: ReportSnapshot,
422
+ options: { screenshotAttached: boolean },
423
+ ): string {
352
424
  const title = escapeHtml(snapshot.title);
353
425
  const dashboardUrl = escapeHtml(snapshot.dashboardUrl);
354
426
  const date = escapeHtml(reportDate(snapshot));
355
427
  const description = snapshot.description
356
428
  ? `<p style="margin:8px 0 18px;color:#525866;font-size:14px;line-height:1.5;">${escapeHtml(snapshot.description)}</p>`
357
429
  : "";
430
+ const screenshotBlock = options.screenshotAttached
431
+ ? `<a href="${dashboardUrl}" style="display:block;text-decoration:none;">
432
+ <img src="cid:${DASHBOARD_REPORT_CID}" alt="${title}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:1px solid #e5e7eb;border-radius:8px;" />
433
+ </a>`
434
+ : `<div style="margin:18px 0;padding:14px 16px;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb;color:#374151;font-size:14px;line-height:1.5;">
435
+ The dashboard image was unavailable for this run. Open the live dashboard to view the latest report.
436
+ </div>`;
358
437
 
359
438
  return `<!doctype html>
360
439
  <html>
@@ -363,9 +442,7 @@ function renderReportEmailHtml(snapshot: ReportSnapshot): string {
363
442
  Here's the report of <a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">${title}</a> for ${date}.
364
443
  </h3>
365
444
  ${description}
366
- <a href="${dashboardUrl}" style="display:block;text-decoration:none;">
367
- <img src="cid:${DASHBOARD_REPORT_CID}" alt="${title}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:1px solid #e5e7eb;border-radius:8px;" />
368
- </a>
445
+ ${screenshotBlock}
369
446
  <p style="margin:18px 0 0;color:#525866;font-size:13px;line-height:1.45;">
370
447
  <a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">Open dashboard</a>
371
448
  </p>
@@ -373,12 +450,19 @@ function renderReportEmailHtml(snapshot: ReportSnapshot): string {
373
450
  </html>`;
374
451
  }
375
452
 
376
- function renderReportText(snapshot: ReportSnapshot): string {
377
- return [
453
+ function renderReportText(
454
+ snapshot: ReportSnapshot,
455
+ options: { screenshotAttached: boolean },
456
+ ): string {
457
+ const lines = [
378
458
  `Daily dashboard report: ${snapshot.title}`,
379
459
  `Date: ${reportDate(snapshot)}`,
380
460
  `Open dashboard: ${snapshot.dashboardUrl}`,
381
- ].join("\n");
461
+ ];
462
+ if (!options.screenshotAttached) {
463
+ lines.push("Dashboard image unavailable for this run.");
464
+ }
465
+ return lines.join("\n");
382
466
  }
383
467
 
384
468
  function reportFilename(title: string): string {
@@ -396,11 +480,14 @@ export async function sendDashboardReportSubscription(
396
480
  dashboardUrl: string;
397
481
  recipientCount: number;
398
482
  screenshotAttached: boolean;
483
+ screenshotMode: "full" | "compact" | "none";
484
+ screenshotError?: string;
399
485
  }> {
400
486
  const snapshot = await collectReportSnapshot(sub);
401
- const png = await captureDashboardPng(sub, snapshot);
402
- const html = renderReportEmailHtml(snapshot);
403
- const text = renderReportText(snapshot);
487
+ const capture = await captureDashboardPngWithFallback(sub, snapshot);
488
+ const screenshotAttached = Boolean(capture.png);
489
+ const html = renderReportEmailHtml(snapshot, { screenshotAttached });
490
+ const text = renderReportText(snapshot, { screenshotAttached });
404
491
  const subject = `Daily dashboard: ${snapshot.title}`;
405
492
 
406
493
  for (const to of sub.recipients) {
@@ -409,21 +496,25 @@ export async function sendDashboardReportSubscription(
409
496
  subject,
410
497
  html,
411
498
  text,
412
- attachments: [
413
- {
414
- filename: reportFilename(snapshot.title),
415
- content: png,
416
- contentType: "image/png",
417
- contentId: DASHBOARD_REPORT_CID,
418
- disposition: "inline",
419
- },
420
- ],
499
+ attachments: capture.png
500
+ ? [
501
+ {
502
+ filename: reportFilename(snapshot.title),
503
+ content: capture.png,
504
+ contentType: "image/png",
505
+ contentId: DASHBOARD_REPORT_CID,
506
+ disposition: "inline",
507
+ },
508
+ ]
509
+ : undefined,
421
510
  });
422
511
  }
423
512
 
424
513
  return {
425
514
  dashboardUrl: snapshot.dashboardUrl,
426
515
  recipientCount: sub.recipients.length,
427
- screenshotAttached: true,
516
+ screenshotAttached,
517
+ screenshotMode: capture.mode,
518
+ ...(capture.error ? { screenshotError: capture.error } : {}),
428
519
  };
429
520
  }
@@ -166,7 +166,7 @@ const REPLAY_RECORDING_DATE_SQL = "substr(started_at, 1, 10)";
166
166
  const REPLAY_TIME_RANGE_FILTER = dashboardTimeRangeFilter(
167
167
  REPLAY_RECORDING_DATE_SQL,
168
168
  );
169
- const REPLAY_RECORDING_FILTER = `chunk_count > 0 AND ${REPLAY_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
169
+ const REPLAY_RECORDING_FILTER = `chunk_count > 0 AND event_count > 0 AND user_id LIKE '%@%' AND ${REPLAY_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
170
170
  const REPLAY_SESSIONS_SQL = `SELECT COUNT(*) AS count FROM session_recordings WHERE ${REPLAY_RECORDING_FILTER}`;
171
171
  const REPLAY_CHUNKS_OVER_TIME_SQL = `SELECT ${REPLAY_RECORDING_DATE_SQL} AS date, SUM(chunk_count) AS count FROM session_recordings WHERE ${REPLAY_RECORDING_FILTER} GROUP BY ${REPLAY_RECORDING_DATE_SQL} ORDER BY date`;
172
172
  const RECENT_REPLAY_SESSIONS_SQL = `SELECT id AS recording_id, session_id, COALESCE(NULLIF(app, ''), NULLIF(template, ''), 'unknown') AS app, COALESCE(NULLIF(user_id, ''), NULLIF(user_key, ''), NULLIF(anonymous_id, ''), 'anonymous') AS visitor, chunk_count AS chunks, event_count AS events, started_at, COALESCE(ended_at, last_ingested_at, started_at) AS last_seen, '/sessions/' || id AS href FROM session_recordings WHERE ${REPLAY_RECORDING_FILTER} ORDER BY last_seen DESC LIMIT 25`;
@@ -194,6 +194,11 @@ function replayString(value: unknown): string | null {
194
194
  return null;
195
195
  }
196
196
 
197
+ function replayEmail(value: unknown): string | null {
198
+ const raw = replayString(value);
199
+ return raw && raw.includes("@") ? raw : null;
200
+ }
201
+
197
202
  function replayInteger(value: unknown): number | null {
198
203
  if (typeof value === "number" && Number.isInteger(value)) return value;
199
204
  if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
@@ -341,6 +346,30 @@ function inferReplayEventCount(inlineData: string | null): number | null {
341
346
  return null;
342
347
  }
343
348
 
349
+ function inferReplayEventTimes(inlineData: string | null): {
350
+ startedAt: string | null;
351
+ endedAt: string | null;
352
+ } {
353
+ if (!inlineData) return { startedAt: null, endedAt: null };
354
+ try {
355
+ const parsed = JSON.parse(inlineData);
356
+ const events = Array.isArray(parsed)
357
+ ? parsed
358
+ : Array.isArray(parsed?.events)
359
+ ? parsed.events
360
+ : [];
361
+ const timestamps = events
362
+ .map((event: unknown) => replayTimestamp(replayRecord(event).timestamp))
363
+ .filter((value: string | null): value is string => Boolean(value));
364
+ return {
365
+ startedAt: replayMinIso(timestamps),
366
+ endedAt: replayMaxIso(timestamps),
367
+ };
368
+ } catch {
369
+ return { startedAt: null, endedAt: null };
370
+ }
371
+ }
372
+
344
373
  function normalizeReplayBlobRef(value: unknown): string | null {
345
374
  const ref = replayString(value);
346
375
  if (!ref) return null;
@@ -393,6 +422,16 @@ function decodeReplayBlobRef(value: string | null): StoredReplayBlobRef | null {
393
422
  }
394
423
  }
395
424
 
425
+ async function deleteReplayBlobHandleQuietly(
426
+ handle: PrivateBlobHandle,
427
+ ): Promise<void> {
428
+ try {
429
+ await deletePrivateBlob(handle);
430
+ } catch {
431
+ // Best-effort rollback cleanup; the original ingest error is more useful.
432
+ }
433
+ }
434
+
396
435
  function parsePositiveIntegerEnv(name: string, fallback: number): number {
397
436
  const raw = process.env[name];
398
437
  if (!raw) return fallback;
@@ -533,14 +572,19 @@ function normalizeReplayChunk(rawValue: unknown): NormalizedSessionReplayChunk {
533
572
  if (!checksum || checksum.length > 128) {
534
573
  throw replayError("Each replay chunk requires a valid checksum", 400);
535
574
  }
575
+ const inferredTimes = inferReplayEventTimes(inlineData);
536
576
 
537
577
  return {
538
578
  seq,
539
579
  checksum,
540
580
  byteLength,
541
581
  eventCount,
542
- startedAt: replayTimestamp(raw.startedAt ?? raw.startTime ?? raw.start),
543
- endedAt: replayTimestamp(raw.endedAt ?? raw.endTime ?? raw.end),
582
+ startedAt:
583
+ replayTimestamp(raw.startedAt ?? raw.startTime ?? raw.start) ??
584
+ inferredTimes.startedAt,
585
+ endedAt:
586
+ replayTimestamp(raw.endedAt ?? raw.endTime ?? raw.end) ??
587
+ inferredTimes.endedAt,
544
588
  storageKind,
545
589
  storageRef,
546
590
  inlineData,
@@ -721,7 +765,17 @@ export function parseSessionReplayIngestPayload(
721
765
  body.status === "completed" || body.completed === true || endedAt
722
766
  ? "completed"
723
767
  : "active";
724
- const userId = replayString(body.userId ?? body.user_id);
768
+ const userEmail =
769
+ replayEmail(body.userEmail ?? body.user_email) ||
770
+ replayEmail(properties.userEmail ?? properties.user_email) ||
771
+ replayEmail(properties.email) ||
772
+ replayEmail(context.userEmail ?? context.user_email) ||
773
+ replayEmail(body.userId ?? body.user_id);
774
+ const userId =
775
+ userEmail ||
776
+ replayString(body.userId ?? body.user_id) ||
777
+ replayString(properties.userId ?? properties.user_id) ||
778
+ replayString(context.userId ?? context.user_id);
725
779
  const anonymousId = replayString(body.anonymousId ?? body.anonymous_id);
726
780
 
727
781
  return {
@@ -730,7 +784,7 @@ export function parseSessionReplayIngestPayload(
730
784
  sessionId,
731
785
  userId,
732
786
  anonymousId,
733
- userKey: userId || anonymousId,
787
+ userKey: userEmail || userId || anonymousId,
734
788
  startedAt,
735
789
  endedAt,
736
790
  durationMs:
@@ -933,6 +987,14 @@ function rowToSessionRecordingSummary(
933
987
  };
934
988
  }
935
989
 
990
+ function isVisibleSessionRecording(row: any): boolean {
991
+ return (
992
+ Boolean(replayEmail(row.userId)) &&
993
+ Number(row.chunkCount ?? 0) > 0 &&
994
+ Number(row.eventCount ?? 0) > 0
995
+ );
996
+ }
997
+
936
998
  function mergeReplayMetadata(
937
999
  existing: Record<string, unknown>,
938
1000
  incoming: Record<string, unknown>,
@@ -951,6 +1013,27 @@ function replayRecordingChangeScope(row: {
951
1013
  return { owner: row.ownerEmail };
952
1014
  }
953
1015
 
1016
+ async function deleteEmptyReplayRecordingPlaceholder(
1017
+ db: any,
1018
+ recording: { id: string; ownerEmail: string; orgId: string | null },
1019
+ ): Promise<void> {
1020
+ await db.delete(schema.sessionRecordings).where(
1021
+ and(
1022
+ eq(schema.sessionRecordings.id, recording.id),
1023
+ eq(schema.sessionRecordings.ownerEmail, recording.ownerEmail),
1024
+ recording.orgId
1025
+ ? eq(schema.sessionRecordings.orgId, recording.orgId)
1026
+ : isNull(schema.sessionRecordings.orgId),
1027
+ eq(schema.sessionRecordings.chunkCount, 0),
1028
+ eq(schema.sessionRecordings.eventCount, 0),
1029
+ sql`not exists (
1030
+ select 1 from ${schema.sessionReplayChunks}
1031
+ where ${schema.sessionReplayChunks.recordingId} = ${schema.sessionRecordings.id}
1032
+ )`,
1033
+ ),
1034
+ );
1035
+ }
1036
+
954
1037
  function escapeSqlLike(value: string): string {
955
1038
  return value.replace(/[\\%_]/g, (match) => `\\${match}`);
956
1039
  }
@@ -1006,10 +1089,11 @@ export async function recordSessionReplayChunks(
1006
1089
  .limit(1);
1007
1090
 
1008
1091
  if (!recording) {
1092
+ const newRecordingId = replayId("sr");
1009
1093
  await db
1010
1094
  .insert(schema.sessionRecordings)
1011
1095
  .values({
1012
- id: replayId("sr"),
1096
+ id: newRecordingId,
1013
1097
  publicKeyId: key.id,
1014
1098
  clientRecordingId: input.clientRecordingId,
1015
1099
  sessionId: input.sessionId,
@@ -1070,54 +1154,76 @@ export async function recordSessionReplayChunks(
1070
1154
  existingChunks.map((chunk: any) => [chunk.seq, chunk]),
1071
1155
  );
1072
1156
  const rowsToInsert: any[] = [];
1157
+ const uploadedBlobHandles: PrivateBlobHandle[] = [];
1073
1158
  let duplicateChunks = 0;
1159
+ const wasEmptyRecording =
1160
+ Number(recording.chunkCount ?? 0) === 0 &&
1161
+ Number(recording.eventCount ?? 0) === 0 &&
1162
+ existingChunks.length === 0;
1074
1163
 
1075
- for (const rawChunk of input.chunks) {
1076
- const chunk = await storeReplayChunkBlob(rawChunk, {
1077
- publicKeyId: key.id,
1078
- recordingId: recording.id,
1079
- ownerEmail: key.ownerEmail,
1080
- orgId: key.orgId,
1081
- });
1082
- const existing = existingBySeq.get(chunk.seq);
1083
- if (existing) {
1084
- if (existing.checksum !== chunk.checksum) {
1164
+ try {
1165
+ for (const rawChunk of input.chunks) {
1166
+ const existing = existingBySeq.get(rawChunk.seq);
1167
+ if (existing) {
1168
+ if (existing.checksum !== rawChunk.checksum) {
1169
+ throw replayError(
1170
+ `Replay chunk ${rawChunk.seq} was already recorded with a different checksum`,
1171
+ 409,
1172
+ );
1173
+ }
1174
+ duplicateChunks += 1;
1175
+ continue;
1176
+ }
1177
+ if (
1178
+ existingChunks.length + rowsToInsert.length >=
1179
+ MAX_REPLAY_CHUNKS_PER_RECORDING
1180
+ ) {
1085
1181
  throw replayError(
1086
- `Replay chunk ${chunk.seq} was already recorded with a different checksum`,
1087
- 409,
1182
+ `Session recordings may contain at most ${MAX_REPLAY_CHUNKS_PER_RECORDING} chunks`,
1183
+ 413,
1088
1184
  );
1089
1185
  }
1090
- duplicateChunks += 1;
1091
- continue;
1092
- }
1093
- if (
1094
- existingChunks.length + rowsToInsert.length >=
1095
- MAX_REPLAY_CHUNKS_PER_RECORDING
1096
- ) {
1097
- throw replayError(
1098
- `Session recordings may contain at most ${MAX_REPLAY_CHUNKS_PER_RECORDING} chunks`,
1099
- 413,
1100
- );
1186
+ const chunk = await storeReplayChunkBlob(rawChunk, {
1187
+ publicKeyId: key.id,
1188
+ recordingId: recording.id,
1189
+ ownerEmail: key.ownerEmail,
1190
+ orgId: key.orgId,
1191
+ });
1192
+ if (rawChunk.storageKind !== "blob" && chunk.storageKind === "blob") {
1193
+ const ref = decodeReplayBlobRef(chunk.storageRef);
1194
+ if (ref) uploadedBlobHandles.push(ref.handle);
1195
+ }
1196
+ rowsToInsert.push({
1197
+ id: replayId("src"),
1198
+ recordingId: recording.id,
1199
+ seq: chunk.seq,
1200
+ checksum: chunk.checksum,
1201
+ byteLength: chunk.byteLength,
1202
+ eventCount: chunk.eventCount,
1203
+ startedAt: chunk.startedAt,
1204
+ endedAt: chunk.endedAt,
1205
+ storageKind: chunk.storageKind,
1206
+ storageRef: chunk.storageRef,
1207
+ inlineData: chunk.inlineData,
1208
+ ownerEmail: key.ownerEmail,
1209
+ orgId: key.orgId,
1210
+ });
1101
1211
  }
1102
- rowsToInsert.push({
1103
- id: replayId("src"),
1104
- recordingId: recording.id,
1105
- seq: chunk.seq,
1106
- checksum: chunk.checksum,
1107
- byteLength: chunk.byteLength,
1108
- eventCount: chunk.eventCount,
1109
- startedAt: chunk.startedAt,
1110
- endedAt: chunk.endedAt,
1111
- storageKind: chunk.storageKind,
1112
- storageRef: chunk.storageRef,
1113
- inlineData: chunk.inlineData,
1114
- ownerEmail: key.ownerEmail,
1115
- orgId: key.orgId,
1116
- });
1117
- }
1118
1212
 
1119
- if (rowsToInsert.length) {
1120
- await db.insert(schema.sessionReplayChunks).values(rowsToInsert);
1213
+ if (rowsToInsert.length) {
1214
+ await db.insert(schema.sessionReplayChunks).values(rowsToInsert);
1215
+ }
1216
+ uploadedBlobHandles.length = 0;
1217
+ } catch (error) {
1218
+ await Promise.all(uploadedBlobHandles.map(deleteReplayBlobHandleQuietly));
1219
+ if (wasEmptyRecording) {
1220
+ await deleteEmptyReplayRecordingPlaceholder(db, {
1221
+ id: recording.id,
1222
+ ownerEmail: key.ownerEmail,
1223
+ orgId: key.orgId,
1224
+ });
1225
+ }
1226
+ throw error;
1121
1227
  }
1122
1228
 
1123
1229
  await db.insert(schema.sessionReplayIngests).values({
@@ -1239,6 +1345,9 @@ export async function listSessionRecordings(
1239
1345
  userEmail: scope.userEmail,
1240
1346
  orgId: scope.orgId ?? undefined,
1241
1347
  }),
1348
+ replayTextContains(schema.sessionRecordings.userId, "@"),
1349
+ gte(schema.sessionRecordings.chunkCount, 1),
1350
+ gte(schema.sessionRecordings.eventCount, 1),
1242
1351
  ];
1243
1352
  if (filters.app)
1244
1353
  conditions.push(eq(schema.sessionRecordings.app, filters.app));
@@ -1300,6 +1409,9 @@ export async function getSessionReplaySummary(
1300
1409
  orgId: scope.orgId ?? undefined,
1301
1410
  });
1302
1411
  if (!access) throw replayError("Session recording not found", 404);
1412
+ if (!isVisibleSessionRecording(access.resource)) {
1413
+ throw replayError("Session recording not found", 404);
1414
+ }
1303
1415
  return rowToSessionRecordingSummary(access.resource, access.role);
1304
1416
  }
1305
1417
 
@@ -87,7 +87,7 @@ export function Layout({ children }: LayoutProps) {
87
87
  )}
88
88
  <div
89
89
  className={cn(
90
- "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 md:static md:z-auto",
90
+ "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
91
91
  mobileSidebarOpen
92
92
  ? "translate-x-0"
93
93
  : "-translate-x-full md:translate-x-0",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-27
4
+ ---
5
+
6
+ Improved mobile navigation chrome and sidebar drawer motion.
@@ -59,15 +59,6 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
59
59
 
60
60
  return (
61
61
  <div className="grid gap-2">
62
- <div className="px-1 pb-1">
63
- <div className="text-sm font-medium text-popover-foreground">
64
- {t("captureInstall.title")}
65
- </div>
66
- <p className="mt-1 text-xs leading-relaxed text-muted-foreground">
67
- {t("captureInstall.description")}
68
- </p>
69
- </div>
70
-
71
62
  {chromeAvailable ? (
72
63
  <a
73
64
  href={clipsChromeExtensionUrl ?? undefined}