@agent-native/core-corpus 0.202.0-nightly-20261006153106 → 0.202.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.
Files changed (68) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +88 -6
  3. package/corpus/templates/analytics/AGENTS.md +3 -2
  4. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  5. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +43 -15
  6. package/corpus/templates/analytics/actions/list-route-performance.ts +53 -0
  7. package/corpus/templates/analytics/actions/list-session-friction.ts +1 -1
  8. package/corpus/templates/analytics/actions/list-session-performance.ts +37 -0
  9. package/corpus/templates/analytics/actions/list-session-recordings.ts +31 -15
  10. package/corpus/templates/analytics/actions/navigate.ts +1 -1
  11. package/corpus/templates/analytics/actions/view-screen.ts +79 -13
  12. package/corpus/templates/analytics/app/components/layout/Layout.tsx +2 -1
  13. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +11 -0
  14. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +4 -0
  15. package/corpus/templates/analytics/app/i18n/en-US.ts +31 -1
  16. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +119 -136
  17. package/corpus/templates/analytics/app/pages/sessions/RoutePerformancePage.tsx +336 -0
  18. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +116 -5
  19. package/corpus/templates/analytics/app/pages/sessions/SessionsLabGate.tsx +52 -0
  20. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +223 -9
  21. package/corpus/templates/analytics/app/routes/sessions.performance.tsx +10 -0
  22. package/corpus/templates/analytics/changelog/2026-10-02-with-the-sessions-triage-lab-on-filter-sessions-by-speed-see.md +6 -0
  23. package/corpus/templates/analytics/server/db/schema.ts +101 -0
  24. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +8 -0
  25. package/corpus/templates/analytics/server/lib/session-event-index.ts +4 -4
  26. package/corpus/templates/analytics/server/lib/session-performance.ts +997 -0
  27. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +14 -3
  28. package/corpus/templates/analytics/server/lib/session-replay.ts +109 -2
  29. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +18 -8
  30. package/corpus/templates/analytics/server/plugins/db.ts +61 -0
  31. package/corpus/templates/analytics/shared/labs.ts +2 -2
  32. package/corpus/templates/analytics/shared/session-events.ts +1 -0
  33. package/corpus/templates/analytics/shared/session-performance.ts +282 -0
  34. package/corpus/templates/analytics/shared/slow-request.ts +14 -0
  35. package/corpus/templates/assets/CHANGELOG.md +6 -0
  36. package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +1 -1
  37. package/corpus/templates/brain/CHANGELOG.md +17 -0
  38. package/corpus/templates/brain/actions/_source-config.ts +4 -4
  39. package/corpus/templates/brain/app/i18n-data.ts +167 -3
  40. package/corpus/templates/brain/app/routes/sources.tsx +158 -4
  41. package/corpus/templates/brain/changelog/2026-10-06-add-zoom-sources-from-the-sources-page.md +6 -0
  42. package/corpus/templates/brain/server/lib/connectors.ts +21 -7
  43. package/corpus/templates/brain/server/lib/zoom.ts +4 -13
  44. package/corpus/templates/brain/shared/zoom-meeting-filter.ts +25 -0
  45. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  46. package/corpus/templates/clips/CHANGELOG.md +3 -0
  47. package/corpus/templates/content/CHANGELOG.md +22 -0
  48. package/corpus/templates/content/server/lib/chatgpt-directory-tools.ts +4 -1
  49. package/corpus/templates/crm/CHANGELOG.md +7 -0
  50. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +5 -1
  51. package/corpus/templates/design/.generated/bridge/hit-test.generated.ts +1 -0
  52. package/corpus/templates/design/CHANGELOG.md +16 -0
  53. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1 -0
  54. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +215 -64
  55. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +12 -2
  56. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +4 -0
  57. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +302 -0
  58. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
  59. package/corpus/templates/design/app/components/design/types.ts +1 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +3 -0
  61. package/corpus/templates/design/changelog/2026-10-05-dropping-a-layer-after-crossing-a-nested-frame-places-it-in-.md +6 -0
  62. package/corpus/templates/design/changelog/2026-10-05-fixed-layer-placement-after-dragging-across-nested-frames.md +6 -0
  63. package/corpus/templates/design/changelog/2026-10-06-dragging-a-selected-nested-frame-now-moves-the-frame-and-sav.md +6 -0
  64. package/corpus/templates/design/server/lib/chatgpt-directory-tools.ts +2 -0
  65. package/corpus/templates/mail/CHANGELOG.md +6 -0
  66. package/corpus/templates/slides/CHANGELOG.md +8 -0
  67. package/corpus/templates/slides/server/lib/chatgpt-directory-tools.ts +4 -1
  68. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 11232
35
+ - template files: 11246
@@ -204,10 +204,11 @@ agent answers about browser recordings in the Analytics template.
204
204
  read. Change weights only in `SESSION_FRICTION_WEIGHTS`.
205
205
  - Every friction surface is behind the Sessions triage Lab, in the UI and in
206
206
  `list-session-recordings`, `list-session-friction`, `view-screen`, and
207
- `get-session-replay-summary`; a 403 names what the Lab gates. With the Lab off, Sessions looks and behaves exactly as before;
208
- ingest still records friction. A link with Lab filters waits up to 5
209
- seconds for the Lab state; one that fails or hangs says so with a retry
210
- and never reads as the Lab being off.
207
+ `get-session-replay-summary`; a 403 names what the Lab gates
208
+ (`assertSessionsTriageLabEnabled` takes every feature the request used).
209
+ With the Lab off, Sessions looks and behaves exactly as before; ingest
210
+ still records friction. Waiting on the Lab state is shared with the speed
211
+ filter; see "Performance In Sessions".
211
212
  - Without a friction filter or sort, the list loads without friction and its
212
213
  rows read friction from `list-session-friction`, one page of ids at most,
213
214
  through the recordings' access filter. A failed friction read then leaves
@@ -226,6 +227,85 @@ agent answers about browser recordings in the Analytics template.
226
227
  `truncated`, and `fullPageAction` reads the whole page. Keep it that way
227
228
  when adding per-row fields: a bigger row means fewer rows, not a lost page.
228
229
 
230
+ ## Performance In Sessions
231
+
232
+ - Core's tracker sends one `web_vitals` event per page view (`route`,
233
+ `navigation_type`, and whichever of `ttfb_ms`, `lcp_ms`, `inp_ms`, `cls` were
234
+ measured; an unmeasured metric is absent, never 0) and an
235
+ `agent-native.vitals` replay marker with the same numbers. `action.response`
236
+ carries the `route` its request started on. `route` is a React Router
237
+ template such as `/sessions/:id`; when no manifest route matches it is
238
+ omitted, never a raw path, which can hold slugs and emails.
239
+ - Requests under `SLOW_REQUEST_THRESHOLD_MS` (`shared/slow-request.ts`, 1 s)
240
+ are sampled at 10% with `sample_weight: 10`; slow, failed, and 4xx responses
241
+ always send with weight 1, so slow-request counts are exact. Use that
242
+ constant for every "slow request" rule. Requests made while the page was
243
+ hidden or cancelled are not counted. Slow requests (the app's own actions,
244
+ 1 s, counted from events) are not friction's stalled requests (any captured
245
+ replay request, `STALLED_REQUEST_THRESHOLD_MS`, 3 s): keep their names,
246
+ fields, columns, and copy apart, and never show one number under the
247
+ other's label.
248
+ - Ingest strips NUL and replaces lone surrogates in `route`, `app`, and the
249
+ session id before hashing, as Postgres would store them; otherwise two
250
+ values that store alike collide in one upsert and fail the batch.
251
+ Measurements above `performanceCeiling` (the open top bucket's floor: 64 s,
252
+ CLS 5) are capped there rather than dropped, and `sample_weight` at 10,000.
253
+ A session value at the ceiling is a floor: summaries list it in `atLeast`
254
+ and the row hint shows it with ≥.
255
+ - `recordSessionPerformance` keeps each session's worst vitals and its slow
256
+ requests in `analytics_session_performance`, inside the ingest transaction
257
+ under a savepoint, with the same gap and coverage rules as the event index.
258
+ `recordRoutePerformance` adds weights to fixed histogram buckets in
259
+ `analytics_route_performance_daily` after commit, in its own short
260
+ transaction. A failed write records a gap: `session_id = ''` marks a
261
+ route day, read as `incompleteDates`; a session id marks that session,
262
+ read as `performance.incomplete` and kept by `slow: any` only, since
263
+ missing data cannot rule it out; `vitals` and `requests` need a measured
264
+ slow value. The slow filter correlates on each recording's own tenant. As
265
+ with event friction, a share grants the recording, not its tenant's
266
+ events: a recording shared from another tenant has no speed summary (null)
267
+ and never matches a slow filter. A summary's `slowRequests` is null when the session made no
268
+ measured request. A null summary is a session never measured: its row says
269
+ "Speed not measured", and a slow match or a range from before coverage
270
+ shows when speed coverage began, so it never reads as fast. On
271
+ `list-session-recordings`, `slow` and `includePerformance` always return
272
+ the paginated shape with `performanceCoverageStartedAt`, as friction does.
273
+ Bucket edges are positional, so changing them needs a new
274
+ `PERFORMANCE_HISTOGRAM_VERSION`.
275
+ - Percentiles interpolate inside one bucket. Past the first bucket each edge
276
+ is at most 28% above the last, so they are within about 28% of the exact
277
+ value; in the first bucket, within 1 ms (CLS 0.001). Keep that step when
278
+ adding edges, and keep every rating threshold an edge. A value in the open
279
+ top bucket is reported as `atLeast`.
280
+ A metric with no samples is null: no data, never fast. Before the migration
281
+ creates `analytics_performance_coverage`, ingest stores events without these
282
+ aggregates and warns, the slow filter matches nothing, and reads report no
283
+ coverage.
284
+ - The replay's slow-request markers come from the `agent-native.slow_request`
285
+ events core records beside each slow `action.response` someone waited for,
286
+ with that event's own duration, status, and outcome. Core and the timeline
287
+ both apply the count's rule (`isWaitedActionResponse`, from core), so a
288
+ marker matches exactly what the row counts and background requests never
289
+ spend the replay's marker budget. Never derive them from replay network events: those stop timing at
290
+ the response headers. Replays from clients without these events show no
291
+ slow-request markers.
292
+ - `slow` and `includePerformance` on `list-session-recordings`,
293
+ `list-session-performance`, `list-route-performance`, the replay's vitals
294
+ and slow-request markers, and `performance` on `get-session-replay-summary`
295
+ exist only while the Sessions triage Lab is on.
296
+ - Lab state never holds up the base list. The page waits for it only when the
297
+ URL carries Lab-only conditions (did/didn't events, friction signals or
298
+ sorts, `slow`), with one wait for all of them, and for at most 5 s; a
299
+ failed or hung read lists sessions without them and says so once, with a
300
+ retry, rather than telling the user to turn on a Lab that may be on.
301
+ Row speed hints load beside the list through `list-session-performance`,
302
+ like row friction, keyed on the visible recording ids; when they fail the
303
+ page says speed data could not load instead of showing no hints, apart
304
+ from any friction failure. `view-screen` reads Lab state in its own `try`
305
+ and reads row friction and speed hints beside the base list, reporting
306
+ `labStateError`, `frictionError`, and `performanceError` separately; its
307
+ `fullPageAction` asks for both.
308
+
229
309
  ## Agent Diagnostics Surface
230
310
 
231
311
  - `buildSessionReplayAgentContext` includes a `diagnostics` section: up to 50
@@ -233,7 +313,9 @@ agent answers about browser recordings in the Analytics template.
233
313
  and truncated flags. Agent-context instructions steer agents to diagnostics
234
314
  as the primary debugging signal.
235
315
  - The agent timeline includes `console-error` / `network-error` markers; error
236
- markers are kept preferentially under the 200-marker cap.
316
+ markers are kept preferentially under the 200-marker cap. App event, Web
317
+ Vitals and slow-request markers belong to the Sessions triage Lab and stay
318
+ off it (`SESSIONS_TRIAGE_MARKER_TAGS`); add any new Lab marker tag there.
237
319
  - `apis.diagnostics` advertises the fuller bounded list:
238
320
  `GET /api/session-replay/agent-diagnostics.json?id=<recordingId>&agent_access=<token>&kind=console|network|all&level=<level>&limit=<n>&offset=<n>&fromMs=<n>&toMs=<n>`
239
321
  (limit defaults to 200, max 500). It uses the same recording-scoped
@@ -295,7 +377,7 @@ agent answers about browser recordings in the Analytics template.
295
377
  widths >= 3,000px. The 2026-07 "ultra-wide replay" bugs (stages rendered
296
378
  3,000–9,500px wide, frozen/teleporting cursors, giant icons) were caused
297
379
  entirely by demo mode's fetch interceptor: its number redactor faked any
298
- integer >= 1000 inside raw replay JSON at *view* time, corrupting Meta /
380
+ integer >= 1000 inside raw replay JSON at _view_ time, corrupting Meta /
299
381
  ViewportResize widths, pointer x/y coordinates, and numeric values inside
300
382
  `_cssText` and SVG attributes before rrweb ever saw the payload (heights
301
383
  below 1000 stayed real, which is why the symptom looked like a viewport
@@ -89,7 +89,7 @@ certified ones); label figures "Unverified" when no live query ran.
89
89
  signals, visitor type, and email domain. Use `paginated: true` for sorted
90
90
  pages with a real total and app counts; the default returns an array.
91
91
  - With the Sessions triage Lab on, `didEvents` / `didNotEvents` filter by
92
- tracked events. Get real names and session counts from
92
+ tracked events and `slow` by speed. Get real names and session counts from
93
93
  `list-session-event-names`, and event health from `list-event-catalog`. Both
94
94
  read Analytics' own index, which covers sessions only from its coverage
95
95
  start. Never query BigQuery for these views.
@@ -98,7 +98,8 @@ certified ones); label figures "Unverified" when no live query ran.
98
98
 
99
99
  - `navigation` exposes the current dashboard, analysis, source, chart, and
100
100
  selection. `navigate` moves the user between supported Analytics surfaces,
101
- `"sessions"`, `"event-catalog"`, `"monitoring"`, and `"agents"`. Use
101
+ `"sessions"`, `"event-catalog"`, `"performance"`, `"monitoring"`, and
102
+ `"agents"`. Use
102
103
  `view-screen` when the
103
104
  active context is unclear.
104
105
  - Clicking a panel stages it as a chat context chip and writes `selected-object`
@@ -11,6 +11,7 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
11
11
 
12
12
  ### Fixed
13
13
 
14
+ - Session replay agent links stay usable with large event lists and longer agent names.
14
15
  - Agent links copied from a dashboard, analysis, or session replay are shorter, so Claude can fetch them without hitting its URL length limit.
15
16
  - Analytics keeps workspace access working across apps with different local organization IDs.
16
17
  - BigQuery dashboard filters work with values that contain apostrophes or backslashes
@@ -18,6 +19,11 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
18
19
 
19
20
  ## 2026-10-02
20
21
 
22
+ ### Added
23
+
24
+ - With the Sessions triage Lab on, filter sessions by speed, see page vitals and slow requests on replays, and compare p50 and p95 load, interaction, and request times per route
25
+ - With the Sessions triage Lab on, rank and filter sessions by friction signals like dead clicks, error toasts, retry loops, failed actions, and agent failures, and open their Monitoring issues from the list.
26
+
21
27
  ### Improved
22
28
 
23
29
  - Builder.io setup now explains free credits and the existing-account option.
@@ -6,7 +6,10 @@ import {
6
6
  import { z } from "zod";
7
7
 
8
8
  import { listRecordingFriction } from "../server/lib/session-friction.js";
9
- import { getSessionReplaySummary } from "../server/lib/session-replay.js";
9
+ import {
10
+ getSessionRecordingPerformance,
11
+ getSessionReplaySummary,
12
+ } from "../server/lib/session-replay.js";
10
13
  import {
11
14
  isSessionsTriageLabEnabled,
12
15
  sessionsTriageReadFailure,
@@ -22,7 +25,7 @@ function resolveScope() {
22
25
 
23
26
  export default defineAction({
24
27
  description:
25
- "Get a scoped summary for one first-party Analytics session replay recording. Does not return raw chunks or storage references. With the Sessions triage Lab on it adds `friction`, the object the replay page's Friction tab shows: score, every signal's count (a null part means not measured, not zero), agent failures grouped by cause, and linked Monitoring error issues (null means the links are unknown, [] means none). A Lab state or friction read that fails returns `labStateError` or `frictionError` instead.",
28
+ "Get a scoped summary for one first-party Analytics session replay recording. Does not return raw chunks or storage references. With the Sessions triage Lab on it adds `friction`, the object the replay page's Friction tab shows: score, every signal's count (a null part means not measured, not zero), agent failures grouped by cause, and linked Monitoring error issues (null means the links are unknown, [] means none). It also adds `performance`, the recording's speed summary (worst page-view vitals, slowRequests of 1 s or more, maxRequestMs, atLeast, incomplete; null when never measured), and `performanceCoverageStartedAt`. A Lab state, friction, or speed read that fails returns `labStateError`, `frictionError`, or `performanceError` instead.",
26
29
  schema: z.object({
27
30
  recordingId: z.string().describe("The session_recordings id"),
28
31
  }),
@@ -47,18 +50,43 @@ export default defineAction({
47
50
  };
48
51
  }
49
52
  if (!labEnabled) return summary;
50
- try {
51
- const friction = (await listRecordingFriction(scope, [summary.id]))[
52
- summary.id
53
- ];
54
- return friction
55
- ? { ...summary, friction }
56
- : { ...summary, frictionError: "Friction read skipped this recording" };
57
- } catch (error) {
58
- return {
59
- ...summary,
60
- frictionError: sessionsTriageReadFailure("friction", LOG_PREFIX, error),
61
- };
62
- }
53
+ const [friction, speed] = await Promise.allSettled([
54
+ listRecordingFriction(scope, [summary.id]),
55
+ getSessionRecordingPerformance(scope, [summary.id]),
56
+ ]);
57
+ const recordingFriction =
58
+ friction.status === "fulfilled" ? friction.value[summary.id] : undefined;
59
+ const recordingSpeed =
60
+ speed.status === "fulfilled"
61
+ ? speed.value.performance[summary.id]
62
+ : undefined;
63
+ return {
64
+ ...summary,
65
+ ...(friction.status === "rejected"
66
+ ? {
67
+ frictionError: sessionsTriageReadFailure(
68
+ "friction",
69
+ LOG_PREFIX,
70
+ friction.reason,
71
+ ),
72
+ }
73
+ : recordingFriction
74
+ ? { friction: recordingFriction }
75
+ : { frictionError: "Friction read skipped this recording" }),
76
+ ...(speed.status === "rejected"
77
+ ? {
78
+ performanceError: sessionsTriageReadFailure(
79
+ "speed",
80
+ LOG_PREFIX,
81
+ speed.reason,
82
+ ),
83
+ }
84
+ : recordingSpeed !== undefined
85
+ ? {
86
+ performance: recordingSpeed,
87
+ performanceCoverageStartedAt: speed.value.coverageStartedAt,
88
+ }
89
+ : { performanceError: "Speed read skipped this recording" }),
90
+ };
63
91
  },
64
92
  });
@@ -0,0 +1,53 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ listRoutePerformance,
10
+ ROUTE_PERFORMANCE_MAX_LIMIT,
11
+ } from "../server/lib/session-performance.js";
12
+ import { assertSessionsTriageLabEnabled } from "../server/lib/sessions-triage-lab.js";
13
+ import { sessionEventBoundSchema } from "../shared/session-events.js";
14
+
15
+ export default defineAction({
16
+ description:
17
+ "List page routes (React Router templates such as /sessions/:id) with p50 and p95 of TTFB, LCP, INP, CLS, and action request duration, plus the count of requests of 1 s or more, from Analytics' own daily histograms. Percentiles are interpolated inside fixed buckets (within about 28% of the exact value; under 1 ms, or CLS 0.001, within that much); request counts are scaled by their sampling. A metric with no samples is null, which means no data, not fast. Returns the most-measured routes first (vitals reported plus requests timed), apps (every app with measured routes in the range, whatever the app filter), coverageStartedAt (null when nothing is measured yet), and incompleteDates whose aggregates missed some events. Ranges cover whole UTC days, at most 90. Requires the Sessions triage Lab.",
18
+ schema: z.object({
19
+ from: sessionEventBoundSchema
20
+ .optional()
21
+ .describe(
22
+ "Inclusive first UTC day as an ISO date or a timestamp with an offset; defaults to 6 days before to",
23
+ ),
24
+ to: sessionEventBoundSchema
25
+ .optional()
26
+ .describe(
27
+ "Inclusive last UTC day as an ISO date or a timestamp with an offset; defaults to today",
28
+ ),
29
+ app: z.string().optional().describe("Optional app filter"),
30
+ limit: z.coerce
31
+ .number()
32
+ .int()
33
+ .min(1)
34
+ .max(ROUTE_PERFORMANCE_MAX_LIMIT)
35
+ .optional()
36
+ .describe("Most routes to return, most-measured first; defaults to 50"),
37
+ }),
38
+ http: { method: "GET" },
39
+ readOnly: true,
40
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
41
+ run: async (args) => {
42
+ const userEmail = getRequestUserEmail();
43
+ if (!userEmail) {
44
+ fail("no authenticated user", {
45
+ errorCode: "unauthenticated",
46
+ statusCode: 401,
47
+ });
48
+ }
49
+ const orgId = getRequestOrgId() || null;
50
+ await assertSessionsTriageLabEnabled(userEmail, orgId, ["speed"]);
51
+ return listRoutePerformance({ userEmail, orgId }, args);
52
+ },
53
+ });
@@ -32,7 +32,7 @@ export default defineAction({
32
32
  });
33
33
  }
34
34
  const orgId = getRequestOrgId() || null;
35
- await assertSessionsTriageLabEnabled(userEmail, orgId, "friction");
35
+ await assertSessionsTriageLabEnabled(userEmail, orgId, ["friction"]);
36
36
  return {
37
37
  friction: await listRecordingFriction({ userEmail, orgId }, recordingIds),
38
38
  };
@@ -0,0 +1,37 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { getSessionRecordingPerformance } from "../server/lib/session-replay.js";
9
+ import { assertSessionsTriageLabEnabled } from "../server/lib/sessions-triage-lab.js";
10
+ import { SESSION_PAGE_SIZE } from "../shared/session-page.js";
11
+
12
+ export default defineAction({
13
+ description:
14
+ "Speed summaries for specific session recordings, such as the rows of one list page: each recording's worst measured page-view vitals (ttfbMs, lcpMs, inpMs, cls), slowRequests (requests of 1 s or more; null when no request was measured), maxRequestMs, atLeast (fields that hit the measurement ceiling, so each is a floor), and incomplete (some measurements failed to save, so it may be slower). A recording that was never measured maps to null; ids you cannot read are absent. Also returns coverageStartedAt. Requires the Sessions triage Lab.",
15
+ schema: z.object({
16
+ recordingIds: z
17
+ .array(z.string().min(1).max(200))
18
+ .max(SESSION_PAGE_SIZE)
19
+ .default([])
20
+ .describe(`Recording ids, at most ${SESSION_PAGE_SIZE}: one list page`),
21
+ }),
22
+ http: { method: "GET" },
23
+ readOnly: true,
24
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
25
+ run: async ({ recordingIds }) => {
26
+ const userEmail = getRequestUserEmail();
27
+ if (!userEmail) {
28
+ fail("no authenticated user", {
29
+ errorCode: "unauthenticated",
30
+ statusCode: 401,
31
+ });
32
+ }
33
+ const orgId = getRequestOrgId() || null;
34
+ await assertSessionsTriageLabEnabled(userEmail, orgId, ["speed"]);
35
+ return getSessionRecordingPerformance({ userEmail, orgId }, recordingIds);
36
+ },
37
+ });
@@ -11,13 +11,17 @@ import {
11
11
  listSessionRecordingsPage,
12
12
  type SessionRecordingSummary,
13
13
  } from "../server/lib/session-replay.js";
14
- import { assertSessionsTriageLabEnabled } from "../server/lib/sessions-triage-lab.js";
14
+ import {
15
+ assertSessionsTriageLabEnabled,
16
+ type SessionsTriageLabFeature,
17
+ } from "../server/lib/sessions-triage-lab.js";
15
18
  import { MAX_SESSION_EVENT_CONDITIONS } from "../shared/session-events.js";
16
19
  import {
17
20
  isSessionFrictionSort,
18
21
  SESSION_FRICTION_SIGNALS,
19
22
  SESSION_FRICTION_SORTS,
20
23
  } from "../shared/session-friction.js";
24
+ import { SLOW_SESSION_FILTERS } from "../shared/session-performance.js";
21
25
 
22
26
  function resolveScope() {
23
27
  const userEmail = getRequestUserEmail();
@@ -111,7 +115,7 @@ export default defineAction({
111
115
  .boolean()
112
116
  .optional()
113
117
  .describe(
114
- "Return recordings, total count, and app counts rather than the legacy recordings array. Friction filters and sorts always return this shape.",
118
+ "Return recordings, total count, and app counts rather than the legacy recordings array. Friction filters and sorts, `slow`, and `includePerformance` always return this shape.",
115
119
  ),
116
120
  status: z.enum(["active", "completed"]).optional(),
117
121
  didEvents: z
@@ -141,6 +145,18 @@ export default defineAction({
141
145
  .describe(
142
146
  "Add each recording's friction: score, signal counts, top signals, failed actions and agent failures grouped by cause, and linked Monitoring error issues. A null part means it was not measured, not zero: `thumbs_down`, `cancelled_runs`, and `quick_backs` can be null alone, and `errorIssues` null means the links are unknown while [] means no issues. Requires the Sessions triage Lab.",
143
147
  ),
148
+ slow: z
149
+ .enum(SLOW_SESSION_FILTERS)
150
+ .optional()
151
+ .describe(
152
+ "Only slow sessions: vitals = a page view with a poor Core Web Vital (LCP > 4 s, INP > 500 ms, CLS > 0.25, or TTFB > 1.8 s); requests = an action request of 1 s or more (not the 3 s `stalled_requests` friction signal); any = either, plus sessions whose measurements failed to save, since missing data cannot rule them out. Sessions without measurements never match. Requires the Sessions triage Lab.",
153
+ ),
154
+ includePerformance: z
155
+ .boolean()
156
+ .optional()
157
+ .describe(
158
+ "Attach each recording's speed summary as performance, plus performanceCoverageStartedAt: worst measured page-view vitals, slowRequests (null when no request was measured), maxRequestMs, atLeast (fields that hit the measurement ceiling, so each is a floor), and incomplete (some measurements failed to save, so it may be slower). performance is null when never measured. Requires the Sessions triage Lab.",
159
+ ),
144
160
  limit: z.coerce.number().int().min(1).max(100).optional().default(50),
145
161
  }),
146
162
  http: { method: "GET" },
@@ -150,28 +166,28 @@ export default defineAction({
150
166
  grounding: true,
151
167
  run: async (args) => {
152
168
  const scope = resolveScope();
153
- const usesEvents = Boolean(
154
- args.didEvents?.length || args.didNotEvents?.length,
155
- );
156
169
  const frictionApplied = Boolean(
157
170
  args.frictionSignals?.length || isSessionFrictionSort(args.sort),
158
171
  );
159
- const usesFriction = frictionApplied || Boolean(args.includeFriction);
160
- if (usesEvents || usesFriction) {
172
+ const labFeatures: SessionsTriageLabFeature[] = [];
173
+ if (args.didEvents?.length || args.didNotEvents?.length) {
174
+ labFeatures.push("events");
175
+ }
176
+ if (frictionApplied || args.includeFriction) labFeatures.push("friction");
177
+ if (args.slow || args.includePerformance) labFeatures.push("speed");
178
+ if (labFeatures.length) {
161
179
  await assertSessionsTriageLabEnabled(
162
180
  scope.userEmail,
163
181
  scope.orgId,
164
- usesEvents && usesFriction
165
- ? "events and friction"
166
- : usesEvents
167
- ? "events"
168
- : "friction",
182
+ labFeatures,
169
183
  );
170
184
  }
171
185
  const { includeFriction, ...filters } = args;
172
- // The legacy array has no room for frictionCoverageStartedAt, and without
173
- // it an empty friction match cannot be told apart from "not measured".
174
- if (!args.paginated && !frictionApplied) {
186
+ // The legacy array has no room for the friction or speed coverage start,
187
+ // and without it an empty match cannot be told apart from "not measured".
188
+ const coverageNeeded =
189
+ frictionApplied || Boolean(args.slow || args.includePerformance);
190
+ if (!args.paginated && !coverageNeeded) {
175
191
  const recordings = await listSessionRecordings(scope, filters);
176
192
  return includeFriction ? withFriction(scope, recordings) : recordings;
177
193
  }
@@ -49,7 +49,7 @@ export default defineAction({
49
49
  .string()
50
50
  .optional()
51
51
  .describe(
52
- "View to navigate to (ask, adhoc, analyses, extensions, sessions, event-catalog, monitoring, agents, data-dictionary, data-sources, settings). event-catalog requires the Sessions triage Lab.",
52
+ "View to navigate to (ask, adhoc, analyses, extensions, sessions, event-catalog, performance, monitoring, agents, data-dictionary, data-sources, settings). event-catalog and performance (per-route Web Vitals and request percentiles) require the Sessions triage Lab.",
53
53
  ),
54
54
  dashboardId: z
55
55
  .string()
@@ -12,6 +12,7 @@ import { getErrorIssue, listErrorIssues } from "../server/lib/error-capture.js";
12
12
  import { listAnalyticsPublicKeys } from "../server/lib/first-party-analytics.js";
13
13
  import { getSessionFrictionDetails } from "../server/lib/session-friction.js";
14
14
  import {
15
+ getSessionRecordingPerformance,
15
16
  getSessionReplaySummary,
16
17
  listSessionRecordingsPage,
17
18
  replayRangeToIso,
@@ -39,6 +40,11 @@ import {
39
40
  SESSION_FRICTION_SIGNAL_PARAM,
40
41
  } from "../shared/session-friction";
41
42
  import { readSessionPage, SESSION_PAGE_SIZE } from "../shared/session-page";
43
+ import {
44
+ isSlowSessionFilter,
45
+ readRoutePerformanceRange,
46
+ routePerformanceRangeBounds,
47
+ } from "../shared/session-performance";
42
48
 
43
49
  const SESSION_FILTER_KEYS = new Set([
44
50
  "range",
@@ -303,9 +309,13 @@ export default defineAction({
303
309
  const urlFrictionSort = isSessionFrictionSort(params.sort)
304
310
  ? params.sort
305
311
  : null;
312
+ const urlSlow = isSlowSessionFilter(params.slow)
313
+ ? params.slow
314
+ : undefined;
306
315
  // Match the page: event conditions, friction filters and sorts,
307
- // and row friction apply only with the Lab on. A failed Lab read
308
- // is reported, and the base list is still read without them.
316
+ // the slow filter, and row friction and speed hints apply only
317
+ // with the Lab on. A failed Lab read is reported, and the base
318
+ // list is still read without them.
309
319
  let triageLabEnabled = false;
310
320
  let labStateError: string | undefined;
311
321
  try {
@@ -331,30 +341,54 @@ export default defineAction({
331
341
  filters.frictionSignals = urlFrictionSignals;
332
342
  }
333
343
  if (urlFrictionSort) filters.sort = urlFrictionSort;
344
+ if (urlSlow) filters.slow = urlSlow;
334
345
  }
335
346
  const result = await listSessionRecordingsPage(scope, {
336
347
  ...filters,
337
348
  limit: SESSION_EXCERPT_SIZE,
338
349
  });
339
350
  screen.sessionReplays = result.recordings;
351
+ // Row friction and speed hints load beside the list, as on the
352
+ // page, so either one failing leaves the list and is reported
353
+ // on its own.
340
354
  let frictionError: string | undefined;
355
+ let performanceError: string | undefined;
356
+ let performanceCoverageStartedAt: string | null | undefined;
341
357
  if (triageLabEnabled) {
342
- try {
343
- const friction = await getSessionFrictionDetails(
358
+ const [friction, speed] = await Promise.allSettled([
359
+ getSessionFrictionDetails(scope, result.recordings),
360
+ getSessionRecordingPerformance(
344
361
  scope,
345
- result.recordings,
346
- );
347
- screen.sessionReplays = result.recordings.map((recording) => ({
348
- ...recording,
349
- friction: friction.get(recording.id),
350
- }));
351
- } catch (error) {
362
+ result.recordings.map((recording) => recording.id),
363
+ ),
364
+ ]);
365
+ if (friction.status === "rejected") {
352
366
  frictionError = sessionsTriageReadFailure(
353
367
  "friction",
354
368
  "[view-screen]",
355
- error,
369
+ friction.reason,
370
+ );
371
+ }
372
+ if (speed.status === "rejected") {
373
+ performanceError = sessionsTriageReadFailure(
374
+ "speed",
375
+ "[view-screen]",
376
+ speed.reason,
356
377
  );
378
+ performanceCoverageStartedAt =
379
+ result.performanceCoverageStartedAt;
380
+ } else {
381
+ performanceCoverageStartedAt = speed.value.coverageStartedAt;
357
382
  }
383
+ screen.sessionReplays = result.recordings.map((recording) => ({
384
+ ...recording,
385
+ ...(friction.status === "fulfilled"
386
+ ? { friction: friction.value.get(recording.id) }
387
+ : {}),
388
+ ...(speed.status === "fulfilled"
389
+ ? { performance: speed.value.performance[recording.id] }
390
+ : {}),
391
+ }));
358
392
  }
359
393
  // The URL's sort and Lab conditions are not what was applied
360
394
  // while the Lab is off, so echo the list's own filters.
@@ -372,6 +406,7 @@ export default defineAction({
372
406
  activeFilters[SESSION_FRICTION_SIGNAL_PARAM] =
373
407
  filters.frictionSignals;
374
408
  }
409
+ if (filters.slow) activeFilters.slow = filters.slow;
375
410
  if (Object.keys(activeFilters).length > 0) {
376
411
  screen.activeFilters = activeFilters;
377
412
  }
@@ -388,11 +423,15 @@ export default defineAction({
388
423
  excerptLimit: SESSION_EXCERPT_SIZE,
389
424
  ...(labStateError ? { labStateError } : {}),
390
425
  ...(frictionError ? { frictionError } : {}),
426
+ ...(performanceError ? { performanceError } : {}),
391
427
  ...(result.frictionCoverageStartedAt !== undefined
392
428
  ? {
393
429
  frictionCoverageStartedAt: result.frictionCoverageStartedAt,
394
430
  }
395
431
  : {}),
432
+ ...(performanceCoverageStartedAt !== undefined
433
+ ? { performanceCoverageStartedAt }
434
+ : {}),
396
435
  ...(urlHasEventConditions && !triageLabEnabled
397
436
  ? { eventConditionsNotApplied: urlEventConditions }
398
437
  : {}),
@@ -405,6 +444,9 @@ export default defineAction({
405
444
  },
406
445
  }
407
446
  : {}),
447
+ ...(urlSlow && !triageLabEnabled
448
+ ? { slowFilterNotApplied: urlSlow }
449
+ : {}),
408
450
  truncated:
409
451
  result.recordings.length <
410
452
  Math.min(SESSION_PAGE_SIZE, Math.max(0, result.total - offset)),
@@ -413,7 +455,9 @@ export default defineAction({
413
455
  args: {
414
456
  paginated: true,
415
457
  ...filters,
416
- ...(triageLabEnabled ? { includeFriction: true } : {}),
458
+ ...(triageLabEnabled
459
+ ? { includeFriction: true, includePerformance: true }
460
+ : {}),
417
461
  limit: SESSION_PAGE_SIZE,
418
462
  },
419
463
  },
@@ -447,6 +491,28 @@ export default defineAction({
447
491
  },
448
492
  }
449
493
  : { labEnabled: false };
494
+ } else if (nav?.view === "performance") {
495
+ screen.page = "route-performance";
496
+ const email = getRequestUserEmail();
497
+ const orgId = getRequestOrgId() || null;
498
+ const labEnabled = email
499
+ ? await isSessionsTriageLabEnabled(email, orgId)
500
+ : false;
501
+ const params = url?.searchParams ?? {};
502
+ const range = readRoutePerformanceRange(params.range);
503
+ screen.routePerformance = labEnabled
504
+ ? {
505
+ range,
506
+ app: params.app || null,
507
+ fullPageAction: {
508
+ name: "list-route-performance",
509
+ args: {
510
+ ...routePerformanceRangeBounds(range),
511
+ ...(params.app ? { app: params.app } : {}),
512
+ },
513
+ },
514
+ }
515
+ : { labEnabled: false };
450
516
  } else if (nav?.view === "monitoring") {
451
517
  screen.page = "monitoring";
452
518
  const monitoringView =
@@ -39,6 +39,7 @@ import { TAB_ID } from "@/lib/tab-id";
39
39
  import { AgentCompletionSound } from "../AgentCompletionSound";
40
40
  import { Header } from "./Header";
41
41
  import { HeaderActionsProvider } from "./HeaderActions";
42
+ import { isSessionReplayRoute } from "./layout-route-policy";
42
43
  import { MobileNav } from "./MobileNav";
43
44
  import { Sidebar } from "./Sidebar";
44
45
 
@@ -160,7 +161,7 @@ function InteractiveLayout({ children }: LayoutProps) {
160
161
  const isExtensionsRoute =
161
162
  location.pathname === "/extensions" ||
162
163
  location.pathname.startsWith("/extensions/");
163
- const isSessionDetailRoute = /^\/sessions\/[^/]+/.test(location.pathname);
164
+ const isSessionDetailRoute = isSessionReplayRoute(location.pathname);
164
165
  const isMonitoringRoute =
165
166
  location.pathname === "/monitoring" ||
166
167
  location.pathname.startsWith("/monitoring/");