@agent-native/core 0.79.21 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/docs/content/deployment.md +82 -82
- package/corpus/core/docs/content/faq.md +1 -1
- package/corpus/core/docs/content/tracking.md +3 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
- package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
- package/corpus/core/src/agent/engine/registry.ts +59 -11
- package/corpus/core/src/client/FeedbackButton.tsx +11 -1
- package/corpus/core/src/client/agent-engine-key.ts +57 -10
- package/corpus/core/src/client/analytics.ts +78 -27
- package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/session-replay.ts +159 -17
- package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
- package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
- package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
- package/corpus/core/src/server/core-routes-plugin.ts +26 -2
- package/corpus/core/src/styles/blocks.css +23 -7
- package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
- package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
- package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
- package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +1 -0
- package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +1 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
- package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
- package/corpus/templates/clips/desktop/src/main.tsx +2 -0
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
- package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
- package/corpus/templates/clips/desktop/vite.config.ts +66 -0
- package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/corpus/templates/forms/actions/export-responses.ts +3 -2
- package/corpus/templates/forms/actions/list-responses.ts +2 -1
- package/corpus/templates/forms/actions/response-insights.ts +13 -4
- package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
- package/corpus/templates/forms/server/lib/integrations.ts +9 -6
- package/corpus/templates/forms/shared/submitter-email.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +1 -1
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
- package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
- package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
- package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
- package/dist/agent/engine/provider-env-vars.d.ts +1 -0
- package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
- package/dist/agent/engine/provider-env-vars.js +1 -0
- package/dist/agent/engine/provider-env-vars.js.map +1 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +39 -12
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/client/FeedbackButton.d.ts.map +1 -1
- package/dist/client/FeedbackButton.js +6 -1
- package/dist/client/FeedbackButton.js.map +1 -1
- package/dist/client/agent-engine-key.d.ts +14 -0
- package/dist/client/agent-engine-key.d.ts.map +1 -1
- package/dist/client/agent-engine-key.js +35 -10
- package/dist/client/agent-engine-key.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +71 -24
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
- package/dist/client/blocks/library/HighlightedCode.js +1 -1
- package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +115 -17
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +42 -6
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
- package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +4 -4
- package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
- package/dist/server/agent-engine-api-key-route.js +71 -11
- package/dist/server/agent-engine-api-key-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/styles/blocks.css +23 -7
- package/docs/content/deployment.md +82 -82
- package/docs/content/faq.md +1 -1
- package/docs/content/tracking.md +3 -3
- 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
|
-
|
|
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
|
|
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(
|
|
337
|
+
page.setDefaultTimeout(timeout);
|
|
310
338
|
await page.emulateMedia({ media: "screen" });
|
|
311
339
|
await page.goto(screenshotUrl.toString(), {
|
|
312
340
|
waitUntil: "domcontentloaded",
|
|
313
|
-
timeout
|
|
341
|
+
timeout,
|
|
314
342
|
});
|
|
315
343
|
|
|
316
344
|
const capture = page.locator("[data-dashboard-report-capture]");
|
|
317
|
-
await capture.waitFor({ state: "visible", timeout
|
|
318
|
-
await waitForDashboardReportReady(page);
|
|
345
|
+
await capture.waitFor({ state: "visible", timeout });
|
|
346
|
+
await waitForDashboardReportReady(page, timeout);
|
|
319
347
|
await scrollDashboardForLazyRendering(page);
|
|
320
|
-
await waitForDashboardReportReady(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
377
|
-
|
|
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
|
-
]
|
|
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
|
|
402
|
-
const
|
|
403
|
-
const
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
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:
|
|
543
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
`
|
|
1087
|
-
|
|
1182
|
+
`Session recordings may contain at most ${MAX_REPLAY_CHUNKS_PER_RECORDING} chunks`,
|
|
1183
|
+
413,
|
|
1088
1184
|
);
|
|
1089
1185
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
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
|
|
|
@@ -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}
|
|
@@ -74,7 +74,9 @@ function shouldDropExpectedMediaPermissionError(
|
|
|
74
74
|
const tags = context.tags ?? {};
|
|
75
75
|
const isPermissionProbe =
|
|
76
76
|
tags.surface === "permission" &&
|
|
77
|
-
(tags.permission
|
|
77
|
+
(!tags.permission ||
|
|
78
|
+
tags.permission === "mic" ||
|
|
79
|
+
tags.permission === "cam");
|
|
78
80
|
const isOptionalBubbleCamera =
|
|
79
81
|
tags.surface === "overlay" && tags.overlayPart === "bubble";
|
|
80
82
|
if (!isPermissionProbe && !isOptionalBubbleCamera) return false;
|
|
@@ -87,11 +89,24 @@ function shouldDropExpectedMediaPermissionError(
|
|
|
87
89
|
return (
|
|
88
90
|
/\bNotAllowedError\b/i.test(text) ||
|
|
89
91
|
/\bPermission (?:denied|dismissed)\b/i.test(text) ||
|
|
92
|
+
/\bPermission denied by system\b/i.test(text) ||
|
|
93
|
+
/\bNotReadableError\b/i.test(text) ||
|
|
94
|
+
/\bCould not start video source\b/i.test(text) ||
|
|
90
95
|
/\b(?:NotFoundError|DevicesNotFoundError)\b/i.test(text) ||
|
|
91
96
|
/\bRequested device not found\b/i.test(text)
|
|
92
97
|
);
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
function shouldDropExpectedMediaPermissionEvent(event: Sentry.Event): boolean {
|
|
101
|
+
if (event.tags?.surface !== "permission") return false;
|
|
102
|
+
const exceptionText = (event.exception?.values ?? [])
|
|
103
|
+
.map((value) => `${value.type ?? ""}: ${value.value ?? ""}`)
|
|
104
|
+
.join(" ");
|
|
105
|
+
return shouldDropExpectedMediaPermissionError(exceptionText, {
|
|
106
|
+
tags: { surface: "permission" },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
function scrubScopeString(value: string): string {
|
|
96
111
|
const scrubbed = scrubUrl(value, { redactAllQueryValues: true });
|
|
97
112
|
if (scrubbed !== value) return scrubbed;
|
|
@@ -185,6 +200,7 @@ export function initExtensionSentry(surface: string): void {
|
|
|
185
200
|
runtime: "chrome-extension",
|
|
186
201
|
surface,
|
|
187
202
|
};
|
|
203
|
+
if (shouldDropExpectedMediaPermissionEvent(event)) return null;
|
|
188
204
|
return scrubEvent(event);
|
|
189
205
|
},
|
|
190
206
|
});
|