@agent-native/core 0.84.67 → 0.85.1

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 (149) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
  4. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
  5. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
  6. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
  7. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
  8. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
  9. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
  10. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
  11. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
  12. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
  13. package/corpus/core/docs/content/tracking.mdx +2 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/action-continuation-guidance.ts +1 -1
  16. package/corpus/core/src/agent/production-agent.ts +222 -38
  17. package/corpus/core/src/agent/run-manager.ts +79 -0
  18. package/corpus/core/src/agent/run-store.ts +122 -8
  19. package/corpus/core/src/agent/types.ts +13 -0
  20. package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
  21. package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
  22. package/corpus/core/src/client/analytics.ts +2 -0
  23. package/corpus/core/src/client/error-format.ts +4 -0
  24. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
  25. package/corpus/core/src/client/session-replay.ts +774 -2
  26. package/corpus/core/src/client/sse-event-processor.ts +65 -9
  27. package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
  28. package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
  29. package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
  30. package/corpus/core/src/server/core-routes-plugin.ts +91 -4
  31. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  32. package/corpus/core/src/server/self-dispatch.ts +29 -0
  33. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  34. package/corpus/templates/analytics/AGENTS.md +13 -0
  35. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  36. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  37. package/corpus/templates/analytics/app/global.css +55 -0
  38. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  39. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  40. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  43. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  46. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  47. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  48. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  53. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  54. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  55. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  56. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  57. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  58. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  59. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  60. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  61. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  62. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  63. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  64. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  65. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  66. package/corpus/templates/plan/server/plans.ts +26 -9
  67. package/dist/agent/action-continuation-guidance.js +1 -1
  68. package/dist/agent/action-continuation-guidance.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +9 -1
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +168 -34
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +29 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +73 -0
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +41 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +105 -7
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +13 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  85. package/dist/client/RunStuckBanner.js +13 -0
  86. package/dist/client/RunStuckBanner.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +410 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +1 -1
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js.map +1 -1
  93. package/dist/client/error-format.d.ts.map +1 -1
  94. package/dist/client/error-format.js +2 -0
  95. package/dist/client/error-format.js.map +1 -1
  96. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  97. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  98. package/dist/client/session-replay.d.ts +36 -0
  99. package/dist/client/session-replay.d.ts.map +1 -1
  100. package/dist/client/session-replay.js +579 -1
  101. package/dist/client/session-replay.js.map +1 -1
  102. package/dist/client/sse-event-processor.d.ts +20 -1
  103. package/dist/client/sse-event-processor.d.ts.map +1 -1
  104. package/dist/client/sse-event-processor.js +42 -11
  105. package/dist/client/sse-event-processor.js.map +1 -1
  106. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  107. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  108. package/dist/client/use-run-stuck-detection.js +18 -4
  109. package/dist/client/use-run-stuck-detection.js.map +1 -1
  110. package/dist/collab/awareness.d.ts +2 -2
  111. package/dist/collab/awareness.d.ts.map +1 -1
  112. package/dist/collab/routes.d.ts +2 -2
  113. package/dist/db/runtime-diagnostics.d.ts +52 -0
  114. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  115. package/dist/db/runtime-diagnostics.js +250 -0
  116. package/dist/db/runtime-diagnostics.js.map +1 -0
  117. package/dist/notifications/routes.d.ts +1 -1
  118. package/dist/observability/routes.d.ts +3 -3
  119. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  120. package/dist/resources/handlers.d.ts +3 -3
  121. package/dist/secrets/routes.d.ts +3 -3
  122. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  123. package/dist/server/agent-chat-plugin.js +101 -2
  124. package/dist/server/agent-chat-plugin.js.map +1 -1
  125. package/dist/server/core-routes-plugin.d.ts +17 -0
  126. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  127. package/dist/server/core-routes-plugin.js +65 -5
  128. package/dist/server/core-routes-plugin.js.map +1 -1
  129. package/dist/server/http-response-telemetry.d.ts +4 -0
  130. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  131. package/dist/server/http-response-telemetry.js +174 -0
  132. package/dist/server/http-response-telemetry.js.map +1 -0
  133. package/dist/server/self-dispatch.d.ts +17 -0
  134. package/dist/server/self-dispatch.d.ts.map +1 -1
  135. package/dist/server/self-dispatch.js +11 -0
  136. package/dist/server/self-dispatch.js.map +1 -1
  137. package/dist/server/transcribe-voice.d.ts +1 -1
  138. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  139. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  140. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  141. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  142. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  143. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  144. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  145. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  146. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  147. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  148. package/docs/content/tracking.mdx +2 -0
  149. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Alert rules can now be viewed and managed from Settings.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Recent Ask chats show a loading placeholder while your chat history loads.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Session replays now capture console logs and network requests so agents can debug user-reported issues from a shared link.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Sharing status labels use neutral icons instead of colored dots.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ The session replay viewer has a Dev Tools panel with console and network tabs.
@@ -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: Array<{
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.sort((a, b) => a.offsetMs - b.offsetMs).slice(0, 200);
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,