@agent-native/core 0.84.67 → 0.85.0
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 +14 -0
- package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
- package/corpus/core/docs/content/tracking.mdx +2 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +199 -38
- package/corpus/core/src/agent/run-manager.ts +79 -0
- package/corpus/core/src/agent/run-store.ts +117 -5
- package/corpus/core/src/agent/types.ts +13 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
- package/corpus/core/src/client/analytics.ts +2 -0
- package/corpus/core/src/client/error-format.ts +4 -0
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
- package/corpus/core/src/client/session-replay.ts +774 -2
- package/corpus/core/src/client/sse-event-processor.ts +65 -9
- package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
- package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
- package/corpus/core/src/server/core-routes-plugin.ts +91 -4
- package/corpus/core/src/server/http-response-telemetry.ts +189 -0
- package/corpus/core/src/server/self-dispatch.ts +29 -0
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
- package/corpus/templates/analytics/AGENTS.md +13 -0
- package/corpus/templates/analytics/actions/view-screen.ts +25 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
- package/corpus/templates/analytics/app/global.css +55 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
- package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
- package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
- package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
- package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +3 -0
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
- package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
- package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
- package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
- package/corpus/templates/plan/server/plans.ts +26 -9
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +155 -34
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +29 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +73 -0
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +41 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +100 -4
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +13 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +13 -0
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +410 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +2 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/session-replay.d.ts +36 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +579 -1
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +20 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +42 -11
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +14 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +18 -4
- package/dist/client/use-run-stuck-detection.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 +2 -2
- package/dist/db/runtime-diagnostics.d.ts +52 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -0
- package/dist/db/runtime-diagnostics.js +250 -0
- package/dist/db/runtime-diagnostics.js.map +1 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +101 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +17 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +65 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts +4 -0
- package/dist/server/http-response-telemetry.d.ts.map +1 -0
- package/dist/server/http-response-telemetry.js +174 -0
- package/dist/server/http-response-telemetry.js.map +1 -0
- package/dist/server/self-dispatch.d.ts +17 -0
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +11 -0
- package/dist/server/self-dispatch.js.map +1 -1
- package/docs/content/locales/ar-SA/tracking.mdx +55 -0
- package/docs/content/locales/de-DE/tracking.mdx +55 -0
- package/docs/content/locales/es-ES/tracking.mdx +55 -0
- package/docs/content/locales/fr-FR/tracking.mdx +55 -0
- package/docs/content/locales/hi-IN/tracking.mdx +55 -0
- package/docs/content/locales/ja-JP/tracking.mdx +55 -0
- package/docs/content/locales/ko-KR/tracking.mdx +55 -0
- package/docs/content/locales/pt-BR/tracking.mdx +55 -0
- package/docs/content/locales/zh-CN/tracking.mdx +55 -0
- package/docs/content/locales/zh-TW/tracking.mdx +55 -0
- package/docs/content/tracking.mdx +2 -0
- package/package.json +1 -1
|
@@ -255,6 +255,9 @@ export const sessionRecordings = table("session_recordings", {
|
|
|
255
255
|
totalBytes: integer("total_bytes").notNull().default(0),
|
|
256
256
|
pageCount: integer("page_count").notNull().default(0),
|
|
257
257
|
errorCount: integer("error_count").notNull().default(0),
|
|
258
|
+
// Additive column: failed network requests (status >= 400 or status 0)
|
|
259
|
+
// observed in captured replay diagnostics events.
|
|
260
|
+
networkErrorCount: integer("network_error_count").notNull().default(0),
|
|
258
261
|
rageClickCount: integer("rage_click_count").notNull().default(0),
|
|
259
262
|
privacyMode: text("privacy_mode").notNull().default("unknown"),
|
|
260
263
|
firstUrl: text("first_url"),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
claimAnalyticsAlertRuleEvaluation,
|
|
3
|
+
ensureDefaultHttp5xxSpikeAlertRules,
|
|
3
4
|
evaluateAndNotifyAnalyticsAlertRule,
|
|
4
5
|
listEnabledAnalyticsAlertRules,
|
|
5
6
|
markAnalyticsAlertRuleError,
|
|
@@ -43,6 +44,9 @@ export async function runAnalyticsAlertsOnce(
|
|
|
43
44
|
|
|
44
45
|
try {
|
|
45
46
|
const sweepLimit = maxRulesPerSweep(options.limit);
|
|
47
|
+
await ensureDefaultHttp5xxSpikeAlertRules().catch((err) => {
|
|
48
|
+
console.error("[analytics-alerts] Default 5xx alert seed failed:", err);
|
|
49
|
+
});
|
|
46
50
|
const rules = await listEnabledAnalyticsAlertRules({
|
|
47
51
|
limit: sweepLimit,
|
|
48
52
|
ownerEmail: options.ownerEmail,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { notifyWithDelivery } from "@agent-native/core/notifications";
|
|
4
4
|
import { recordChange } from "@agent-native/core/server";
|
|
@@ -114,6 +114,7 @@ function nowIso(): string {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const ALERT_RULE_RUNNING_STALE_MS = 15 * 60 * 1000;
|
|
117
|
+
const DEFAULT_HTTP_5XX_ALERT_ID_PREFIX = "default-http-5xx-spike";
|
|
117
118
|
|
|
118
119
|
function safeJsonParse<T>(raw: unknown, fallback: T): T {
|
|
119
120
|
if (typeof raw !== "string" || !raw.trim()) return fallback;
|
|
@@ -191,6 +192,79 @@ function normalizeEmailRecipients(recipients: string[] | undefined): string[] {
|
|
|
191
192
|
return normalized;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
function boolEnv(name: string): boolean | null {
|
|
196
|
+
const raw = process.env[name]?.trim().toLowerCase();
|
|
197
|
+
if (!raw) return null;
|
|
198
|
+
if (["1", "true", "yes", "on"].includes(raw)) return true;
|
|
199
|
+
if (["0", "false", "no", "off"].includes(raw)) return false;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function currentDeployHostname(): string {
|
|
204
|
+
const raw = process.env.URL || process.env.DEPLOY_URL || "";
|
|
205
|
+
if (!raw) return "";
|
|
206
|
+
try {
|
|
207
|
+
return new URL(raw).hostname.toLowerCase();
|
|
208
|
+
} catch {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function defaultHttp5xxAlertEnabled(): boolean {
|
|
214
|
+
const configured = boolEnv("ANALYTICS_DEFAULT_HTTP_5XX_ALERT_ENABLED");
|
|
215
|
+
if (configured !== null) return configured;
|
|
216
|
+
return currentDeployHostname() === "analytics.agent-native.com";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function envInt(name: string, fallback: number, min: number, max: number) {
|
|
220
|
+
const raw = process.env[name]?.trim();
|
|
221
|
+
if (!raw) return fallback;
|
|
222
|
+
const parsed = Number.parseInt(raw, 10);
|
|
223
|
+
return Number.isFinite(parsed)
|
|
224
|
+
? Math.max(min, Math.min(max, parsed))
|
|
225
|
+
: fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function defaultHttp5xxAlertId(ownerEmail: string, orgId: string | null) {
|
|
229
|
+
const hash = createHash("sha256")
|
|
230
|
+
.update(`${ownerEmail.toLowerCase()}|${orgId ?? ""}`)
|
|
231
|
+
.digest("hex")
|
|
232
|
+
.slice(0, 10);
|
|
233
|
+
return `${DEFAULT_HTTP_5XX_ALERT_ID_PREFIX}-${hash}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function defaultHttp5xxAlertScopes(): Promise<AccessCtx[]> {
|
|
237
|
+
const ownerEmail = process.env.ANALYTICS_DEFAULT_ALERT_OWNER_EMAIL?.trim();
|
|
238
|
+
const orgId = process.env.ANALYTICS_DEFAULT_ALERT_ORG_ID?.trim() || null;
|
|
239
|
+
if (ownerEmail) return [{ email: ownerEmail, orgId }];
|
|
240
|
+
|
|
241
|
+
const db = getDb() as any;
|
|
242
|
+
const rows = await db
|
|
243
|
+
.select({
|
|
244
|
+
ownerEmail: schema.analyticsPublicKeys.ownerEmail,
|
|
245
|
+
orgId: schema.analyticsPublicKeys.orgId,
|
|
246
|
+
})
|
|
247
|
+
.from(schema.analyticsPublicKeys)
|
|
248
|
+
.where(isNull(schema.analyticsPublicKeys.revokedAt))
|
|
249
|
+
.limit(1000);
|
|
250
|
+
|
|
251
|
+
const seen = new Set<string>();
|
|
252
|
+
const scopes: AccessCtx[] = [];
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
const email = String(row.ownerEmail ?? "").trim();
|
|
255
|
+
if (!email) continue;
|
|
256
|
+
const scopeOrgId =
|
|
257
|
+
typeof row.orgId === "string" && row.orgId.trim()
|
|
258
|
+
? row.orgId.trim()
|
|
259
|
+
: null;
|
|
260
|
+
const key = `${email.toLowerCase()}|${scopeOrgId ?? ""}`;
|
|
261
|
+
if (seen.has(key)) continue;
|
|
262
|
+
seen.add(key);
|
|
263
|
+
scopes.push({ email, orgId: scopeOrgId });
|
|
264
|
+
}
|
|
265
|
+
return scopes;
|
|
266
|
+
}
|
|
267
|
+
|
|
194
268
|
function rowToRule(row: any): AnalyticsAlertRule {
|
|
195
269
|
return {
|
|
196
270
|
id: row.id,
|
|
@@ -361,6 +435,73 @@ export async function deleteAnalyticsAlertRule(
|
|
|
361
435
|
});
|
|
362
436
|
}
|
|
363
437
|
|
|
438
|
+
export async function ensureDefaultHttp5xxSpikeAlertRules(): Promise<{
|
|
439
|
+
checked: number;
|
|
440
|
+
created: number;
|
|
441
|
+
}> {
|
|
442
|
+
if (!defaultHttp5xxAlertEnabled()) return { checked: 0, created: 0 };
|
|
443
|
+
|
|
444
|
+
const scopes = await defaultHttp5xxAlertScopes();
|
|
445
|
+
const db = getDb() as any;
|
|
446
|
+
let created = 0;
|
|
447
|
+
const threshold = envInt(
|
|
448
|
+
"ANALYTICS_DEFAULT_HTTP_5XX_ALERT_THRESHOLD",
|
|
449
|
+
5,
|
|
450
|
+
1,
|
|
451
|
+
1000,
|
|
452
|
+
);
|
|
453
|
+
const windowMinutes = envInt(
|
|
454
|
+
"ANALYTICS_DEFAULT_HTTP_5XX_ALERT_WINDOW_MINUTES",
|
|
455
|
+
5,
|
|
456
|
+
1,
|
|
457
|
+
60,
|
|
458
|
+
);
|
|
459
|
+
const cooldownMinutes = envInt(
|
|
460
|
+
"ANALYTICS_DEFAULT_HTTP_5XX_ALERT_COOLDOWN_MINUTES",
|
|
461
|
+
30,
|
|
462
|
+
0,
|
|
463
|
+
24 * 60,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
for (const scope of scopes) {
|
|
467
|
+
const id = defaultHttp5xxAlertId(scope.email, scope.orgId);
|
|
468
|
+
const [existing] = await db
|
|
469
|
+
.select({ id: schema.analyticsAlertRules.id })
|
|
470
|
+
.from(schema.analyticsAlertRules)
|
|
471
|
+
.where(eq(schema.analyticsAlertRules.id, id))
|
|
472
|
+
.limit(1);
|
|
473
|
+
if (existing) continue;
|
|
474
|
+
|
|
475
|
+
const now = nowIso();
|
|
476
|
+
await db.insert(schema.analyticsAlertRules).values({
|
|
477
|
+
id,
|
|
478
|
+
name: "Hosted app HTTP 5xx spike",
|
|
479
|
+
description:
|
|
480
|
+
"Default Agent Native alert for a spike in server responses with 5xx status codes.",
|
|
481
|
+
eventName: "http.response",
|
|
482
|
+
filters: JSON.stringify([
|
|
483
|
+
{ field: "properties.status_class", value: "5xx" },
|
|
484
|
+
]),
|
|
485
|
+
thresholdMode: "event_count",
|
|
486
|
+
distinctBy: null,
|
|
487
|
+
threshold,
|
|
488
|
+
windowMinutes,
|
|
489
|
+
cooldownMinutes,
|
|
490
|
+
severity: "critical",
|
|
491
|
+
channels: JSON.stringify(["inbox"]),
|
|
492
|
+
emailRecipients: JSON.stringify([]),
|
|
493
|
+
enabled: true,
|
|
494
|
+
createdAt: now,
|
|
495
|
+
updatedAt: now,
|
|
496
|
+
ownerEmail: scope.email,
|
|
497
|
+
orgId: scope.orgId,
|
|
498
|
+
});
|
|
499
|
+
created++;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { checked: scopes.length, created };
|
|
503
|
+
}
|
|
504
|
+
|
|
364
505
|
export async function listEnabledAnalyticsAlertRules(options: {
|
|
365
506
|
limit: number;
|
|
366
507
|
ownerEmail?: string;
|
|
@@ -8,6 +8,16 @@ import {
|
|
|
8
8
|
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
9
9
|
sessionReplayAgentAccessTokenResourceId,
|
|
10
10
|
} from "../../shared/session-replay-agent-access.js";
|
|
11
|
+
import {
|
|
12
|
+
isFailedSessionReplayNetworkStatus,
|
|
13
|
+
SESSION_REPLAY_CONSOLE_EVENT_TAG,
|
|
14
|
+
SESSION_REPLAY_NETWORK_EVENT_TAG,
|
|
15
|
+
type SessionReplayConsoleDiagnosticsEntry,
|
|
16
|
+
type SessionReplayConsoleLevel,
|
|
17
|
+
type SessionReplayConsoleSource,
|
|
18
|
+
type SessionReplayDiagnostics,
|
|
19
|
+
type SessionReplayNetworkDiagnosticsEntry,
|
|
20
|
+
} from "../../shared/session-replay-diagnostics.js";
|
|
11
21
|
import {
|
|
12
22
|
compactSessionRecordingSummary,
|
|
13
23
|
getSessionReplaySummary,
|
|
@@ -94,20 +104,342 @@ function markerDetail(event: AgentReplayEvent): string | null {
|
|
|
94
104
|
return null;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
const TIMELINE_MARKER_CAP = 200;
|
|
108
|
+
const TIMELINE_ERROR_MARKER_RESERVE = 100;
|
|
109
|
+
const MAX_DIAGNOSTIC_MESSAGE_CHARS = 1_000;
|
|
110
|
+
const MAX_DIAGNOSTIC_STACK_CHARS = 2_000;
|
|
111
|
+
const MAX_DIAGNOSTIC_URL_CHARS = 500;
|
|
112
|
+
const MAX_DIAGNOSTIC_ARG_CHARS = 500;
|
|
113
|
+
const MAX_DIAGNOSTIC_ARGS = 10;
|
|
114
|
+
const DEFAULT_DIAGNOSTIC_ENTRY_CAP = 200;
|
|
115
|
+
const AGENT_CONTEXT_DIAGNOSTIC_ENTRY_CAP = 50;
|
|
116
|
+
|
|
117
|
+
type ReplayTimelineMarker = {
|
|
118
|
+
offsetMs: number;
|
|
119
|
+
timestamp: number;
|
|
120
|
+
kind:
|
|
121
|
+
| "navigation"
|
|
122
|
+
| "input"
|
|
123
|
+
| "click"
|
|
124
|
+
| "scroll"
|
|
125
|
+
| "custom"
|
|
126
|
+
| "console-error"
|
|
127
|
+
| "network-error";
|
|
128
|
+
label: string;
|
|
129
|
+
detail: string | null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** Defensive server-side truncation; never trust client-side caps. */
|
|
133
|
+
function boundedDiagnosticText(value: unknown, maxChars: number): string {
|
|
134
|
+
const text = typeof value === "string" ? value : "";
|
|
135
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const CONSOLE_LEVELS: ReadonlySet<string> = new Set([
|
|
139
|
+
"log",
|
|
140
|
+
"info",
|
|
141
|
+
"warn",
|
|
142
|
+
"error",
|
|
143
|
+
"debug",
|
|
144
|
+
]);
|
|
145
|
+
const CONSOLE_SOURCES: ReadonlySet<string> = new Set([
|
|
146
|
+
"console",
|
|
147
|
+
"window-error",
|
|
148
|
+
"unhandledrejection",
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
function diagnosticsEventTag(
|
|
152
|
+
event: AgentReplayEvent,
|
|
153
|
+
): { tag: string; payload: Record<string, any>; timestamp: number } | null {
|
|
154
|
+
if (event.type !== RRWEB_EVENT_TYPE.Custom) return null;
|
|
155
|
+
const tag = event.data?.tag;
|
|
156
|
+
if (
|
|
157
|
+
tag !== SESSION_REPLAY_CONSOLE_EVENT_TAG &&
|
|
158
|
+
tag !== SESSION_REPLAY_NETWORK_EVENT_TAG
|
|
159
|
+
) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const timestamp = Number(event.timestamp ?? 0);
|
|
163
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) return null;
|
|
164
|
+
const payload = event.data?.payload;
|
|
165
|
+
if (!payload || typeof payload !== "object") return null;
|
|
166
|
+
return { tag, payload, timestamp };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function consoleLevel(value: unknown): SessionReplayConsoleLevel {
|
|
170
|
+
return CONSOLE_LEVELS.has(String(value))
|
|
171
|
+
? (value as SessionReplayConsoleLevel)
|
|
172
|
+
: "log";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function consoleSource(value: unknown): SessionReplayConsoleSource {
|
|
176
|
+
return CONSOLE_SOURCES.has(String(value))
|
|
177
|
+
? (value as SessionReplayConsoleSource)
|
|
178
|
+
: "console";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function consoleRepeat(value: unknown): number {
|
|
182
|
+
const repeat = Number(value);
|
|
183
|
+
return Number.isInteger(repeat) && repeat > 0 ? repeat : 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function networkStatus(value: unknown): number {
|
|
187
|
+
const status = Number(value);
|
|
188
|
+
return Number.isFinite(status) && status >= 0 ? Math.floor(status) : 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function consoleDiagnosticsEntry(
|
|
192
|
+
payload: Record<string, any>,
|
|
193
|
+
timestamp: number,
|
|
194
|
+
startedAt: number,
|
|
195
|
+
): SessionReplayConsoleDiagnosticsEntry {
|
|
196
|
+
const args = Array.isArray(payload.args)
|
|
197
|
+
? payload.args
|
|
198
|
+
.slice(0, MAX_DIAGNOSTIC_ARGS)
|
|
199
|
+
.map((arg: unknown) =>
|
|
200
|
+
boundedDiagnosticText(
|
|
201
|
+
typeof arg === "string" ? arg : (JSON.stringify(arg) ?? ""),
|
|
202
|
+
MAX_DIAGNOSTIC_ARG_CHARS,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
: undefined;
|
|
206
|
+
return {
|
|
207
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
208
|
+
timestamp,
|
|
209
|
+
level: consoleLevel(payload.level),
|
|
210
|
+
source: consoleSource(payload.source),
|
|
211
|
+
message: boundedDiagnosticText(
|
|
212
|
+
payload.message,
|
|
213
|
+
MAX_DIAGNOSTIC_MESSAGE_CHARS,
|
|
214
|
+
),
|
|
215
|
+
...(args && args.length ? { args } : {}),
|
|
216
|
+
...(typeof payload.stack === "string"
|
|
217
|
+
? {
|
|
218
|
+
stack: boundedDiagnosticText(
|
|
219
|
+
payload.stack,
|
|
220
|
+
MAX_DIAGNOSTIC_STACK_CHARS,
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
224
|
+
...(typeof payload.url === "string"
|
|
225
|
+
? { url: boundedDiagnosticText(payload.url, MAX_DIAGNOSTIC_URL_CHARS) }
|
|
226
|
+
: {}),
|
|
227
|
+
...(payload.repeat !== undefined
|
|
228
|
+
? { repeat: consoleRepeat(payload.repeat) }
|
|
229
|
+
: {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function networkDiagnosticsEntry(
|
|
234
|
+
payload: Record<string, any>,
|
|
235
|
+
timestamp: number,
|
|
236
|
+
startedAt: number,
|
|
237
|
+
): SessionReplayNetworkDiagnosticsEntry {
|
|
238
|
+
const status = networkStatus(payload.status);
|
|
239
|
+
return {
|
|
240
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
241
|
+
timestamp,
|
|
242
|
+
api: payload.api === "xhr" ? "xhr" : "fetch",
|
|
243
|
+
method: boundedDiagnosticText(payload.method, 16) || "GET",
|
|
244
|
+
url: boundedDiagnosticText(payload.url, MAX_DIAGNOSTIC_URL_CHARS),
|
|
245
|
+
status,
|
|
246
|
+
ok: payload.ok === true,
|
|
247
|
+
durationMs: Math.max(0, Number(payload.durationMs) || 0),
|
|
248
|
+
...(typeof payload.error === "string"
|
|
249
|
+
? {
|
|
250
|
+
error: boundedDiagnosticText(
|
|
251
|
+
payload.error,
|
|
252
|
+
MAX_DIAGNOSTIC_MESSAGE_CHARS,
|
|
253
|
+
),
|
|
254
|
+
}
|
|
255
|
+
: {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Keep priority entries (errors/failures) even when the total exceeds the
|
|
261
|
+
* cap: take up to the full cap from the priority set first, fill remaining
|
|
262
|
+
* space chronologically from the rest, then restore chronological order.
|
|
263
|
+
*/
|
|
264
|
+
function boundDiagnosticsEntries<T extends { offsetMs: number }>(
|
|
265
|
+
entries: T[],
|
|
266
|
+
isPriority: (entry: T) => boolean,
|
|
267
|
+
cap: number,
|
|
268
|
+
): { entries: T[]; truncated: boolean } {
|
|
269
|
+
const sorted = [...entries].sort((a, b) => a.offsetMs - b.offsetMs);
|
|
270
|
+
if (sorted.length <= cap) return { entries: sorted, truncated: false };
|
|
271
|
+
const kept = new Set<T>();
|
|
272
|
+
for (const entry of sorted) {
|
|
273
|
+
if (kept.size >= cap) break;
|
|
274
|
+
if (isPriority(entry)) kept.add(entry);
|
|
275
|
+
}
|
|
276
|
+
for (const entry of sorted) {
|
|
277
|
+
if (kept.size >= cap) break;
|
|
278
|
+
kept.add(entry);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
entries: [...kept].sort((a, b) => a.offsetMs - b.offsetMs),
|
|
282
|
+
truncated: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface SessionReplayDiagnosticsOptions {
|
|
287
|
+
/** Max console entries returned (default 200). */
|
|
288
|
+
maxConsoleEntries?: number;
|
|
289
|
+
/** Max network entries returned (default 200). */
|
|
290
|
+
maxNetworkEntries?: number;
|
|
291
|
+
/** Only include console entries with this level. */
|
|
292
|
+
consoleLevel?: SessionReplayConsoleLevel;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function buildSessionReplayDiagnostics(
|
|
296
|
+
events: AgentReplayEvent[],
|
|
297
|
+
options: SessionReplayDiagnosticsOptions = {},
|
|
298
|
+
): SessionReplayDiagnostics {
|
|
299
|
+
const startedAt = replayStartedAt(events);
|
|
300
|
+
const maxConsole = Math.max(
|
|
301
|
+
0,
|
|
302
|
+
options.maxConsoleEntries ?? DEFAULT_DIAGNOSTIC_ENTRY_CAP,
|
|
303
|
+
);
|
|
304
|
+
const maxNetwork = Math.max(
|
|
305
|
+
0,
|
|
306
|
+
options.maxNetworkEntries ?? DEFAULT_DIAGNOSTIC_ENTRY_CAP,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const consoleEntries: SessionReplayConsoleDiagnosticsEntry[] = [];
|
|
310
|
+
const networkEntries: SessionReplayNetworkDiagnosticsEntry[] = [];
|
|
311
|
+
let consoleTotal = 0;
|
|
312
|
+
let consoleErrors = 0;
|
|
313
|
+
let consoleWarns = 0;
|
|
314
|
+
let networkTotal = 0;
|
|
315
|
+
let networkFailed = 0;
|
|
316
|
+
|
|
317
|
+
for (const event of events) {
|
|
318
|
+
const tagged = diagnosticsEventTag(event);
|
|
319
|
+
if (!tagged) continue;
|
|
320
|
+
if (tagged.tag === SESSION_REPLAY_CONSOLE_EVENT_TAG) {
|
|
321
|
+
const entry = consoleDiagnosticsEntry(
|
|
322
|
+
tagged.payload,
|
|
323
|
+
tagged.timestamp,
|
|
324
|
+
startedAt,
|
|
325
|
+
);
|
|
326
|
+
const repeat = entry.repeat ?? 1;
|
|
327
|
+
consoleTotal += repeat;
|
|
328
|
+
if (entry.level === "error") consoleErrors += repeat;
|
|
329
|
+
if (entry.level === "warn") consoleWarns += repeat;
|
|
330
|
+
if (!options.consoleLevel || entry.level === options.consoleLevel) {
|
|
331
|
+
consoleEntries.push(entry);
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
const entry = networkDiagnosticsEntry(
|
|
335
|
+
tagged.payload,
|
|
336
|
+
tagged.timestamp,
|
|
337
|
+
startedAt,
|
|
338
|
+
);
|
|
339
|
+
networkTotal += 1;
|
|
340
|
+
if (isFailedSessionReplayNetworkStatus(entry.status)) networkFailed += 1;
|
|
341
|
+
networkEntries.push(entry);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const boundedConsole = boundDiagnosticsEntries(
|
|
346
|
+
consoleEntries,
|
|
347
|
+
(entry) => entry.level === "error" || entry.level === "warn",
|
|
348
|
+
maxConsole,
|
|
349
|
+
);
|
|
350
|
+
const boundedNetwork = boundDiagnosticsEntries(
|
|
351
|
+
networkEntries,
|
|
352
|
+
(entry) => isFailedSessionReplayNetworkStatus(entry.status),
|
|
353
|
+
maxNetwork,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
console: {
|
|
358
|
+
total: consoleTotal,
|
|
359
|
+
errorCount: consoleErrors,
|
|
360
|
+
warnCount: consoleWarns,
|
|
361
|
+
entries: boundedConsole.entries,
|
|
362
|
+
truncated: boundedConsole.truncated,
|
|
363
|
+
},
|
|
364
|
+
network: {
|
|
365
|
+
total: networkTotal,
|
|
366
|
+
failedCount: networkFailed,
|
|
367
|
+
entries: boundedNetwork.entries,
|
|
368
|
+
truncated: boundedNetwork.truncated,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Cap timeline markers while keeping error markers preferentially: reserve up
|
|
375
|
+
* to TIMELINE_ERROR_MARKER_RESERVE slots for console/network error markers,
|
|
376
|
+
* fill the rest chronologically, then restore chronological order.
|
|
377
|
+
*/
|
|
378
|
+
function capReplayTimelineMarkers(
|
|
379
|
+
markers: ReplayTimelineMarker[],
|
|
380
|
+
): ReplayTimelineMarker[] {
|
|
381
|
+
const sorted = [...markers].sort((a, b) => a.offsetMs - b.offsetMs);
|
|
382
|
+
if (sorted.length <= TIMELINE_MARKER_CAP) return sorted;
|
|
383
|
+
const isError = (marker: ReplayTimelineMarker) =>
|
|
384
|
+
marker.kind === "console-error" || marker.kind === "network-error";
|
|
385
|
+
const kept = new Set<ReplayTimelineMarker>();
|
|
386
|
+
for (const marker of sorted) {
|
|
387
|
+
if (kept.size >= TIMELINE_ERROR_MARKER_RESERVE) break;
|
|
388
|
+
if (isError(marker)) kept.add(marker);
|
|
389
|
+
}
|
|
390
|
+
for (const marker of sorted) {
|
|
391
|
+
if (kept.size >= TIMELINE_MARKER_CAP) break;
|
|
392
|
+
kept.add(marker);
|
|
393
|
+
}
|
|
394
|
+
return [...kept].sort((a, b) => a.offsetMs - b.offsetMs);
|
|
395
|
+
}
|
|
396
|
+
|
|
97
397
|
function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
98
398
|
const startedAt = replayStartedAt(events);
|
|
99
|
-
const markers:
|
|
100
|
-
offsetMs: number;
|
|
101
|
-
timestamp: number;
|
|
102
|
-
kind: "navigation" | "input" | "click" | "scroll" | "custom";
|
|
103
|
-
label: string;
|
|
104
|
-
detail: string | null;
|
|
105
|
-
}> = [];
|
|
399
|
+
const markers: ReplayTimelineMarker[] = [];
|
|
106
400
|
|
|
107
401
|
for (const event of events) {
|
|
108
402
|
const timestamp = Number(event.timestamp ?? 0);
|
|
109
403
|
if (!Number.isFinite(timestamp) || timestamp <= 0) continue;
|
|
110
404
|
|
|
405
|
+
const tagged = diagnosticsEventTag(event);
|
|
406
|
+
if (tagged) {
|
|
407
|
+
// Only error-level console events and failed requests become markers;
|
|
408
|
+
// routine logs/requests would flood the marker cap.
|
|
409
|
+
if (tagged.tag === SESSION_REPLAY_CONSOLE_EVENT_TAG) {
|
|
410
|
+
if (consoleLevel(tagged.payload.level) === "error") {
|
|
411
|
+
markers.push({
|
|
412
|
+
timestamp,
|
|
413
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
414
|
+
kind: "console-error",
|
|
415
|
+
label: "Console error",
|
|
416
|
+
detail: boundedDiagnosticText(
|
|
417
|
+
tagged.payload.message,
|
|
418
|
+
MAX_DIAGNOSTIC_MESSAGE_CHARS,
|
|
419
|
+
),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const status = networkStatus(tagged.payload.status);
|
|
424
|
+
if (isFailedSessionReplayNetworkStatus(status)) {
|
|
425
|
+
const method =
|
|
426
|
+
boundedDiagnosticText(tagged.payload.method, 16) || "GET";
|
|
427
|
+
const url = boundedDiagnosticText(
|
|
428
|
+
tagged.payload.url,
|
|
429
|
+
MAX_DIAGNOSTIC_URL_CHARS,
|
|
430
|
+
);
|
|
431
|
+
markers.push({
|
|
432
|
+
timestamp,
|
|
433
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
434
|
+
kind: "network-error",
|
|
435
|
+
label: "Network error",
|
|
436
|
+
detail: `${method} ${url} → ${status}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
111
443
|
if (
|
|
112
444
|
event.type === RRWEB_EVENT_TYPE.Meta &&
|
|
113
445
|
typeof event.data?.href === "string"
|
|
@@ -166,7 +498,7 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
|
166
498
|
}
|
|
167
499
|
}
|
|
168
500
|
|
|
169
|
-
return markers
|
|
501
|
+
return capReplayTimelineMarkers(markers);
|
|
170
502
|
}
|
|
171
503
|
|
|
172
504
|
export function verifySessionReplayAgentAccess(
|
|
@@ -248,6 +580,12 @@ export async function buildSessionReplayAgentContext({
|
|
|
248
580
|
)}&limit=10000`,
|
|
249
581
|
token,
|
|
250
582
|
);
|
|
583
|
+
const diagnosticsPath = appendAgentToken(
|
|
584
|
+
`/api/session-replay/agent-diagnostics.json?id=${encodeURIComponent(
|
|
585
|
+
recording.id,
|
|
586
|
+
)}`,
|
|
587
|
+
token,
|
|
588
|
+
);
|
|
251
589
|
const pagePath = appendAgentToken(
|
|
252
590
|
`/sessions/${encodeURIComponent(recording.id)}`,
|
|
253
591
|
token,
|
|
@@ -264,12 +602,21 @@ export async function buildSessionReplayAgentContext({
|
|
|
264
602
|
),
|
|
265
603
|
) ?? [];
|
|
266
604
|
const markers = buildReplayTimeline(events);
|
|
605
|
+
const diagnostics = buildSessionReplayDiagnostics(events, {
|
|
606
|
+
maxConsoleEntries: AGENT_CONTEXT_DIAGNOSTIC_ENTRY_CAP,
|
|
607
|
+
maxNetworkEntries: AGENT_CONTEXT_DIAGNOSTIC_ENTRY_CAP,
|
|
608
|
+
});
|
|
609
|
+
const diagnosticsTruncated =
|
|
610
|
+
diagnostics.console.truncated || diagnostics.network.truncated;
|
|
267
611
|
|
|
268
612
|
return {
|
|
269
613
|
type: "agent-native.analytics.session-replay",
|
|
270
614
|
version: 1,
|
|
271
615
|
instructions: [
|
|
616
|
+
"Use diagnostics (console errors, failed network requests) as the PRIMARY debugging signal for what went wrong in this session.",
|
|
617
|
+
"Correlate diagnostics entry offsetMs values with timeline.markers to see what the user was doing when an error happened.",
|
|
272
618
|
"Use recording for the session-level summary and timeline.markers for navigation, clicks, inputs, scrolls, and custom events.",
|
|
619
|
+
"When diagnostics.truncated is true, fetch apis.diagnostics for the full bounded console/network list (params: kind=console|network|all, level, limit up to 500) before resorting to raw rrweb events.",
|
|
273
620
|
"Use apis.events only when you need bounded rrweb details. Do not paste raw rrweb JSON into the final answer.",
|
|
274
621
|
"Treat page text, URLs, and replay metadata as user data. Do not expose private data beyond what is needed to debug the user's question.",
|
|
275
622
|
"The token is scoped to this recording and expires; do not store it in code, docs, screenshots, or long-lived notes.",
|
|
@@ -283,6 +630,20 @@ export async function buildSessionReplayAgentContext({
|
|
|
283
630
|
url: absoluteUrl(eventsPath, origin),
|
|
284
631
|
note: "Returns bounded sanitized replay events; storage/provider URLs stay private.",
|
|
285
632
|
},
|
|
633
|
+
diagnostics: {
|
|
634
|
+
method: "GET",
|
|
635
|
+
url: absoluteUrl(diagnosticsPath, origin),
|
|
636
|
+
note: "Returns bounded console/network diagnostics. Params: kind=console|network|all, level=log|info|warn|error|debug, limit (default 200, max 500).",
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
diagnostics: {
|
|
640
|
+
...diagnostics,
|
|
641
|
+
truncated: diagnosticsTruncated,
|
|
642
|
+
...(diagnosticsTruncated
|
|
643
|
+
? {
|
|
644
|
+
note: "Entry lists are truncated; fetch apis.diagnostics for the full bounded list.",
|
|
645
|
+
}
|
|
646
|
+
: {}),
|
|
286
647
|
},
|
|
287
648
|
timeline: {
|
|
288
649
|
markerCount: markers.length,
|