@agent-native/core 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.
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
+ error?: undefined;
142
143
  accepted: boolean;
143
144
  duplicate: boolean;
144
145
  eventId: string;
145
- error?: undefined;
146
146
  }>;
@@ -53,6 +53,7 @@ export type ConfigureTrackingOptions = {
53
53
  llmConnectionStatus?: boolean;
54
54
  authSessionRefresh?: boolean;
55
55
  pageviewTracking?: boolean;
56
+ webVitals?: boolean;
56
57
  sessionReplay?: boolean | SessionReplayOptions;
57
58
  errorCapture?: boolean | ErrorCaptureConfigOptions;
58
59
  };
@@ -1,5 +1,5 @@
1
1
  import { recordTrackingEvent } from "../observability/tracing.js";
2
- import { AGENT_NATIVE_LIFECYCLE_EVENTS, AGENT_SIGNALS_PAGEVIEW_PROPERTY, AGENT_SIGNALS_VERSION, canonicalTrackingEvent, legacyLifecycleEvent, normalizeTrackingDimension, PAGE_LOAD_PAGEVIEW_PROPERTY, withCanonicalTrackingProperties, } from "../shared/analytics-events.js";
2
+ import { AGENT_NATIVE_LIFECYCLE_EVENTS, AGENT_SIGNALS_PAGEVIEW_PROPERTY, AGENT_SIGNALS_VERSION, canonicalTrackingEvent, legacyLifecycleEvent, normalizeTrackingDimension, PAGE_LOAD_PAGEVIEW_PROPERTY, isWaitedActionResponse, SLOW_ACTION_RESPONSE_MS, withCanonicalTrackingProperties, } from "../shared/analytics-events.js";
3
3
  import { ANALYTICS_CLIENT_PLATFORM_PROPERTY, } from "../shared/analytics-platform.js";
4
4
  import { hasAttributionSource } from "../shared/attribution-source.js";
5
5
  import { resolveLaneEndpoint } from "../shared/environment-lanes.js";
@@ -18,7 +18,9 @@ import { scheduleAfterPaint } from "./use-after-paint.js";
18
18
  export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
19
19
  import { fetchAgentEngineStatus, fetchAuthSessionStatus, } from "./client-status-requests.js";
20
20
  import { firstPartyHosts, installErrorCapture, } from "./error-capture.js";
21
+ import { currentRouteTemplate } from "./route-template.js";
21
22
  import { scrubUrl } from "./url-scrub.js";
23
+ import { installWebVitals, } from "./web-vitals.js";
22
24
  export { scrubUrl } from "./url-scrub.js";
23
25
  export { addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, } from "./error-capture.js";
24
26
  export { getSessionReplayContext, getSessionReplayUrl, } from "./session-replay-context.js";
@@ -1105,6 +1107,8 @@ export function configureTracking(options) {
1105
1107
  }
1106
1108
  if (options.pageviewTracking !== false) {
1107
1109
  installPageviewTracking();
1110
+ if (options.webVitals !== false)
1111
+ installWebVitalsTracking();
1108
1112
  }
1109
1113
  maybeInstallSessionReplay(options.sessionReplay, {
1110
1114
  endpoint: options.endpoint,
@@ -1570,6 +1574,11 @@ function inferTemplateName(properties) {
1570
1574
  }
1571
1575
  return app;
1572
1576
  }
1577
+ /**
1578
+ * Events that name their page only by route template. A path can hold a slug
1579
+ * or an email, so it never rides along, not even from an app's default props.
1580
+ */
1581
+ const ROUTE_ONLY_EVENT_NAMES = new Set(["web_vitals"]);
1573
1582
  function resolveProps(name, params) {
1574
1583
  if (name === "session_replay_started") {
1575
1584
  return params?.recording_attempt_id === undefined
@@ -1625,13 +1634,18 @@ function resolveProps(name, params) {
1625
1634
  });
1626
1635
  const withIdentity = applyTrackingIdentity(standard);
1627
1636
  const identity = _trackingIdentity;
1628
- return {
1637
+ const resolved = {
1629
1638
  ...withIdentity,
1630
1639
  ...(getTrackingUserId() ? { user_id: getTrackingUserId() } : {}),
1631
1640
  ...(identity?.userEmail ? { user_email: identity.userEmail } : {}),
1632
1641
  ...(identity?.orgId ? { workspace_id: identity.orgId } : {}),
1633
1642
  [ANALYTICS_CLIENT_PLATFORM_PROPERTY]: getAnalyticsClientPlatform(_configuredAnalyticsClientPlatform ?? undefined),
1634
1643
  };
1644
+ if (ROUTE_ONLY_EVENT_NAMES.has(name)) {
1645
+ delete resolved.url;
1646
+ delete resolved.path;
1647
+ }
1648
+ return resolved;
1635
1649
  }
1636
1650
  function sessionReplayTrackingProperties() {
1637
1651
  const module = _sessionReplayModuleForCapture;
@@ -1831,20 +1845,54 @@ function installPageviewTracking() {
1831
1845
  window.history.pushState = function pushState(...args) {
1832
1846
  const result = originalPushState.apply(this, args);
1833
1847
  syncTrackingContentCaptureForLocation();
1848
+ state.webVitals?.navigate("push");
1834
1849
  schedulePageview("pushState");
1835
1850
  return result;
1836
1851
  };
1837
1852
  window.history.replaceState = function replaceState(...args) {
1838
1853
  const result = originalReplaceState.apply(this, args);
1839
1854
  syncTrackingContentCaptureForLocation();
1855
+ state.webVitals?.navigate("replace");
1840
1856
  schedulePageview("replaceState");
1841
1857
  return result;
1842
1858
  };
1843
1859
  window.addEventListener("popstate", () => {
1844
1860
  syncTrackingContentCaptureForLocation();
1861
+ state.webVitals?.navigate("push");
1845
1862
  schedulePageview("popstate");
1846
1863
  });
1847
1864
  }
1865
+ function webVitalsLocation() {
1866
+ return {
1867
+ route: currentRouteTemplate(),
1868
+ pathname: window.location.pathname,
1869
+ };
1870
+ }
1871
+ function reportPageViewVitals(vitals) {
1872
+ _sessionReplayModuleForCapture?.emitSessionReplayWebVitals?.(vitals);
1873
+ trackEvent("web_vitals", {
1874
+ ...(vitals.route ? { route: vitals.route } : {}),
1875
+ navigation_type: vitals.navigationType,
1876
+ ttfb_ms: vitals.ttfbMs,
1877
+ lcp_ms: vitals.lcpMs,
1878
+ inp_ms: vitals.inpMs,
1879
+ cls: vitals.cls,
1880
+ });
1881
+ }
1882
+ function installWebVitalsTracking() {
1883
+ const state = getPageviewTrackingState();
1884
+ if (state.webVitalsInstalled)
1885
+ return;
1886
+ state.webVitalsInstalled = true;
1887
+ if (isLocalAnalyticsHostname(window.location.hostname))
1888
+ return;
1889
+ try {
1890
+ state.webVitals = installWebVitals(webVitalsLocation, reportPageViewVitals);
1891
+ }
1892
+ catch (error) {
1893
+ console.warn("[analytics] Web Vitals capture is unavailable:", error);
1894
+ }
1895
+ }
1848
1896
  function sendAgentNativeAnalytics(name, properties) {
1849
1897
  if (isSyntheticBrowserTraffic())
1850
1898
  return;
@@ -1912,13 +1960,20 @@ const REPLAY_UNMARKED_EVENT_NAMES = new Set([
1912
1960
  "session status",
1913
1961
  "session_status",
1914
1962
  "action.response",
1963
+ "web_vitals",
1915
1964
  "agent_chat_lifecycle",
1916
1965
  "session_replay_started",
1917
1966
  "session replay upload rejected",
1918
1967
  "session_replay_upload_rejected",
1919
1968
  AGENT_NATIVE_EXCEPTION_EVENT_NAME,
1920
1969
  ]);
1921
- function markTrackedEventInSessionReplay(name) {
1970
+ function markTrackedEventInSessionReplay(name, props) {
1971
+ if (name === "action.response" &&
1972
+ typeof props.duration_ms === "number" &&
1973
+ props.duration_ms >= SLOW_ACTION_RESPONSE_MS &&
1974
+ isWaitedActionResponse(props)) {
1975
+ _sessionReplayModuleForCapture?.emitSessionReplaySlowRequest?.(props);
1976
+ }
1922
1977
  if (REPLAY_UNMARKED_EVENT_NAMES.has(name))
1923
1978
  return;
1924
1979
  _sessionReplayModuleForCapture?.emitSessionReplayAnalyticsEvent?.(name);
@@ -1945,8 +2000,9 @@ function trackBrowserEvent(name, params, markInReplay) {
1945
2000
  sendGtag: !gtagNameMatchesCanonical,
1946
2001
  });
1947
2002
  }
1948
- if (markInReplay)
1949
- markTrackedEventInSessionReplay(canonical?.name ?? name);
2003
+ if (markInReplay) {
2004
+ markTrackedEventInSessionReplay(canonical?.name ?? name, props);
2005
+ }
1950
2006
  void recordTrackingEvent(name, props, "client");
1951
2007
  const lifecycle = legacyLifecycleEvent(name, props);
1952
2008
  // The alias describes the same moment, so it gets no second replay marker.
@@ -0,0 +1,16 @@
1
+ type ManifestRoute = {
2
+ id: string;
3
+ parentId?: string;
4
+ path?: string;
5
+ index?: boolean;
6
+ };
7
+ /**
8
+ * The route template a path renders, such as `/sessions/:id`, so telemetry
9
+ * groups pages without carrying ids from the URL. Null when no manifest route
10
+ * matches: a raw path keeps slugs, short ids, and emails, so telemetry omits
11
+ * the route instead.
12
+ */
13
+ export declare function routeTemplateForPath(pathname: string, routes: Record<string, ManifestRoute> | undefined, basename?: string): string | null;
14
+ /** Browser only: the route template of the current location, if any. */
15
+ export declare function currentRouteTemplate(): string | null;
16
+ export {};
@@ -0,0 +1,127 @@
1
+ const MAX_ROUTE_TEMPLATE_LENGTH = 200;
2
+ const PARAM_SEGMENT = /^:[\w-]+$/;
3
+ let cachedRoutes;
4
+ let cachedRouteCount = 0;
5
+ let cachedPatterns = [];
6
+ // Mirrors React Router's branch ranking so the template is the route that
7
+ // rendered, without importing react-router (an optional peer of core).
8
+ function scorePattern(segments, index) {
9
+ let score = segments.length;
10
+ if (segments.includes("*"))
11
+ score -= 2;
12
+ if (index)
13
+ score += 2;
14
+ for (const segment of segments) {
15
+ if (segment === "*")
16
+ continue;
17
+ score += PARAM_SEGMENT.test(segment) ? 3 : segment === "" ? 1 : 10;
18
+ }
19
+ return score;
20
+ }
21
+ function explodeOptionalSegments(segments) {
22
+ if (!segments.length)
23
+ return [[]];
24
+ const [first, ...rest] = segments;
25
+ const tails = explodeOptionalSegments(rest);
26
+ if (!first.endsWith("?"))
27
+ return tails.map((tail) => [first, ...tail]);
28
+ const required = first.slice(0, -1);
29
+ return [...tails.map((tail) => [required, ...tail]), ...tails];
30
+ }
31
+ function fullPathSegments(route, routes) {
32
+ const chain = [];
33
+ const seen = new Set();
34
+ let current = route;
35
+ while (current && !seen.has(current.id)) {
36
+ seen.add(current.id);
37
+ if (current.path)
38
+ chain.unshift(current.path);
39
+ current = current.parentId ? routes[current.parentId] : undefined;
40
+ }
41
+ return chain
42
+ .join("/")
43
+ .split("/")
44
+ .filter((segment) => segment !== "");
45
+ }
46
+ // React Router lazy route discovery adds routes to the same manifest object,
47
+ // so the same object with more routes is a new manifest.
48
+ function routePatterns(routes) {
49
+ const manifestRoutes = Object.values(routes);
50
+ if (routes === cachedRoutes && manifestRoutes.length === cachedRouteCount) {
51
+ return cachedPatterns;
52
+ }
53
+ const patterns = [];
54
+ for (const route of manifestRoutes) {
55
+ if (route.path == null && !route.index)
56
+ continue;
57
+ for (const segments of explodeOptionalSegments(fullPathSegments(route, routes))) {
58
+ patterns.push({
59
+ segments,
60
+ score: scorePattern(segments, Boolean(route.index)),
61
+ });
62
+ }
63
+ }
64
+ patterns.sort((a, b) => b.score - a.score);
65
+ cachedRoutes = routes;
66
+ cachedRouteCount = manifestRoutes.length;
67
+ cachedPatterns = patterns;
68
+ return patterns;
69
+ }
70
+ function matches(pattern, pathSegments) {
71
+ for (let index = 0; index < pattern.length; index += 1) {
72
+ const segment = pattern[index];
73
+ if (segment === "*")
74
+ return true;
75
+ const value = pathSegments[index];
76
+ if (value === undefined)
77
+ return false;
78
+ if (PARAM_SEGMENT.test(segment))
79
+ continue;
80
+ if (segment.toLowerCase() !== value.toLowerCase())
81
+ return false;
82
+ }
83
+ return pattern.length === pathSegments.length;
84
+ }
85
+ function stripBasename(pathname, basename) {
86
+ const base = basename.replace(/\/+$/, "");
87
+ if (!base)
88
+ return pathname;
89
+ const lower = pathname.toLowerCase();
90
+ const lowerBase = base.toLowerCase();
91
+ if (lower === lowerBase)
92
+ return "/";
93
+ return lower.startsWith(`${lowerBase}/`) ? pathname.slice(base.length) : null;
94
+ }
95
+ function decodeSegment(segment) {
96
+ try {
97
+ return decodeURIComponent(segment);
98
+ }
99
+ catch {
100
+ return segment;
101
+ }
102
+ }
103
+ /**
104
+ * The route template a path renders, such as `/sessions/:id`, so telemetry
105
+ * groups pages without carrying ids from the URL. Null when no manifest route
106
+ * matches: a raw path keeps slugs, short ids, and emails, so telemetry omits
107
+ * the route instead.
108
+ */
109
+ export function routeTemplateForPath(pathname, routes, basename = "") {
110
+ const relative = stripBasename(pathname, basename);
111
+ if (!routes || relative === null)
112
+ return null;
113
+ const pathSegments = relative
114
+ .split("/")
115
+ .filter((segment) => segment !== "")
116
+ .map(decodeSegment);
117
+ const match = routePatterns(routes).find((pattern) => matches(pattern.segments, pathSegments));
118
+ return match
119
+ ? `/${match.segments.join("/")}`.slice(0, MAX_ROUTE_TEMPLATE_LENGTH)
120
+ : null;
121
+ }
122
+ /** Browser only: the route template of the current location, if any. */
123
+ export function currentRouteTemplate() {
124
+ const routes = window.__reactRouterManifest?.routes;
125
+ const basename = window.__reactRouterContext?.basename;
126
+ return routeTemplateForPath(window.location.pathname, routes, basename ?? "");
127
+ }
@@ -72,6 +72,8 @@ export declare const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
72
72
  export declare const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
73
73
  export declare const SESSION_REPLAY_AGENT_CHAT_EVENT_TAG = "agent-native.chat";
74
74
  export declare const SESSION_REPLAY_ANALYTICS_EVENT_TAG = "agent-native.event";
75
+ export declare const SESSION_REPLAY_VITALS_EVENT_TAG = "agent-native.vitals";
76
+ export declare const SESSION_REPLAY_SLOW_REQUEST_EVENT_TAG = "agent-native.slow_request";
75
77
  export declare function getSessionReplaySamplingScore(sessionId: string, salt?: string): number;
76
78
  export declare function shouldSampleSessionReplay(sessionId: string, sampleRate?: number, salt?: string): boolean;
77
79
  export declare function flushSessionReplay(reason?: string): Promise<void>;
@@ -94,6 +96,34 @@ export declare function emitSessionReplayException(input: {
94
96
  * is recorded; event properties stay out of the replay.
95
97
  */
96
98
  export declare function emitSessionReplayAnalyticsEvent(name: string): void;
99
+ export type SessionReplaySlowRequest = {
100
+ action?: unknown;
101
+ method?: unknown;
102
+ duration_ms?: unknown;
103
+ status_code?: unknown;
104
+ outcome?: unknown;
105
+ page_hidden?: unknown;
106
+ };
107
+ /**
108
+ * Mark a slow `action.response` on the replay timeline with that event's own
109
+ * timing, which runs to the end of the body; a network event stops at the
110
+ * headers. Only these operational fields are recorded, and the marker shares
111
+ * the analytics marker budget.
112
+ */
113
+ export declare function emitSessionReplaySlowRequest(input: SessionReplaySlowRequest): void;
114
+ export type SessionReplayWebVitals = {
115
+ route?: string;
116
+ navigationType: string;
117
+ ttfbMs?: number;
118
+ lcpMs?: number;
119
+ inpMs?: number;
120
+ cls?: number;
121
+ };
122
+ /**
123
+ * Mark a finished page view's Web Vitals on the replay timeline. Shares the
124
+ * analytics marker budget, since each page view adds one.
125
+ */
126
+ export declare function emitSessionReplayWebVitals(input: SessionReplayWebVitals): void;
97
127
  export type SessionReplayAgentChatEvent = {
98
128
  phase: "surface-mounted" | "run-observed" | "run-stopped";
99
129
  surface: string | number;
@@ -72,6 +72,8 @@ export const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
72
72
  export const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
73
73
  export const SESSION_REPLAY_AGENT_CHAT_EVENT_TAG = "agent-native.chat";
74
74
  export const SESSION_REPLAY_ANALYTICS_EVENT_TAG = "agent-native.event";
75
+ export const SESSION_REPLAY_VITALS_EVENT_TAG = "agent-native.vitals";
76
+ export const SESSION_REPLAY_SLOW_REQUEST_EVENT_TAG = "agent-native.slow_request";
75
77
  const SESSION_REPLAY_LIFECYCLE_EVENT_TAG = "agent-native.session_replay";
76
78
  const DEFAULT_MAX_CONSOLE_EVENTS = 1000;
77
79
  const DEFAULT_MAX_NETWORK_EVENTS = 2000;
@@ -2190,6 +2192,23 @@ function installNetworkCapture(state, captureOptions) {
2190
2192
  ? (captureOptions.maxErrorBodyLength ?? DEFAULT_MAX_ERROR_BODY_LENGTH)
2191
2193
  : null;
2192
2194
  const restores = [];
2195
+ // A background tab throttles timers, so a request that was ever hidden did
2196
+ // not keep anyone waiting for its duration. Checking visibility only at the
2197
+ // end misses a tab that was hidden and came back.
2198
+ let hiddenEpoch = 0;
2199
+ const countHidden = () => {
2200
+ if (document.visibilityState === "hidden")
2201
+ hiddenEpoch += 1;
2202
+ };
2203
+ document.addEventListener("visibilitychange", countHidden);
2204
+ restores.push(() => document.removeEventListener("visibilitychange", countHidden));
2205
+ const watchHidden = () => {
2206
+ const hiddenAtStart = document.visibilityState !== "visible";
2207
+ const epochAtStart = hiddenEpoch;
2208
+ return () => hiddenAtStart ||
2209
+ epochAtStart !== hiddenEpoch ||
2210
+ document.visibilityState !== "visible";
2211
+ };
2193
2212
  // A request the browser cancels because the page is leaving fails exactly
2194
2213
  // like a network error. WebKit cancels right after beforeunload, Chromium
2195
2214
  // after pagehide; neither listener costs the page its bfcache eligibility.
@@ -2217,7 +2236,7 @@ function installNetworkCapture(state, captureOptions) {
2217
2236
  target.removeEventListener(type, listener, { capture: true });
2218
2237
  }
2219
2238
  });
2220
- const recordRequest = (api, method, rawUrl, status, ok, durationMs, error, responseBody) => {
2239
+ const recordRequest = (api, method, rawUrl, status, ok, durationMs, pageHidden, error, responseBody) => {
2221
2240
  if (stopped)
2222
2241
  return;
2223
2242
  try {
@@ -2232,6 +2251,7 @@ function installNetworkCapture(state, captureOptions) {
2232
2251
  status,
2233
2252
  ok,
2234
2253
  durationMs: Math.max(0, Math.round(durationMs)),
2254
+ ...(pageHidden ? { pageHidden: true } : {}),
2235
2255
  ...(status === 0 && pageLeaving ? { pageLeaving: true } : {}),
2236
2256
  ...(error
2237
2257
  ? {
@@ -2299,6 +2319,7 @@ function installNetworkCapture(state, captureOptions) {
2299
2319
  return originalFetch.call(self, input, init);
2300
2320
  }
2301
2321
  const startedAt = performance.now();
2322
+ const wasHidden = watchHidden();
2302
2323
  const result = originalFetch.call(self, input, init);
2303
2324
  if (!result || typeof result.then !== "function") {
2304
2325
  return result;
@@ -2306,6 +2327,7 @@ function installNetworkCapture(state, captureOptions) {
2306
2327
  return result.then((response) => {
2307
2328
  try {
2308
2329
  const durationMs = performance.now() - startedAt;
2330
+ const pageHidden = wasHidden();
2309
2331
  if (errorBodyCap !== null && response.status >= 500) {
2310
2332
  let clone = null;
2311
2333
  try {
@@ -2321,18 +2343,18 @@ function installNetworkCapture(state, captureOptions) {
2321
2343
  });
2322
2344
  Promise.race([bodyPromise, timeoutPromise])
2323
2345
  .then((responseBody) => {
2324
- recordRequest("fetch", method, url, response.status, response.ok, durationMs, undefined, responseBody);
2346
+ recordRequest("fetch", method, url, response.status, response.ok, durationMs, pageHidden, undefined, responseBody);
2325
2347
  })
2326
2348
  .catch(() => {
2327
2349
  // never affect the caller
2328
2350
  });
2329
2351
  }
2330
2352
  else {
2331
- recordRequest("fetch", method, url, response.status, response.ok, durationMs);
2353
+ recordRequest("fetch", method, url, response.status, response.ok, durationMs, pageHidden);
2332
2354
  }
2333
2355
  }
2334
2356
  else {
2335
- recordRequest("fetch", method, url, response.status, response.ok, durationMs);
2357
+ recordRequest("fetch", method, url, response.status, response.ok, durationMs, pageHidden);
2336
2358
  }
2337
2359
  }
2338
2360
  catch {
@@ -2341,7 +2363,7 @@ function installNetworkCapture(state, captureOptions) {
2341
2363
  return response;
2342
2364
  }, (error) => {
2343
2365
  try {
2344
- recordRequest("fetch", method, url, 0, false, performance.now() - startedAt, error instanceof Error ? error.message : String(error));
2366
+ recordRequest("fetch", method, url, 0, false, performance.now() - startedAt, wasHidden(), error instanceof Error ? error.message : String(error));
2345
2367
  }
2346
2368
  catch {
2347
2369
  // never affect the caller
@@ -2374,6 +2396,7 @@ function installNetworkCapture(state, captureOptions) {
2374
2396
  const info = xhrInfo.get(this);
2375
2397
  if (info && !stopped && !replayCaptureInternal) {
2376
2398
  const startedAt = performance.now();
2399
+ const wasHidden = watchHidden();
2377
2400
  let errorMessage;
2378
2401
  const markError = (message) => () => {
2379
2402
  errorMessage = message;
@@ -2392,7 +2415,7 @@ function installNetworkCapture(state, captureOptions) {
2392
2415
  if (errorBodyCap !== null && !errorMessage && status >= 500) {
2393
2416
  responseBody = readXhrErrorBody(this, errorBodyCap);
2394
2417
  }
2395
- recordRequest("xhr", info.method, info.url, effectiveStatus, !errorMessage && status >= 200 && status < 300, performance.now() - startedAt, errorMessage, responseBody);
2418
+ recordRequest("xhr", info.method, info.url, effectiveStatus, !errorMessage && status >= 200 && status < 300, performance.now() - startedAt, wasHidden(), errorMessage, responseBody);
2396
2419
  xhrInfo.delete(this);
2397
2420
  }, { once: true });
2398
2421
  }
@@ -2821,6 +2844,20 @@ export function emitSessionReplayException(input) {
2821
2844
  ...(input.url ? { url: input.url } : {}),
2822
2845
  });
2823
2846
  }
2847
+ function reserveAnalyticsMarker(state) {
2848
+ if (state.analyticsEventCount >= MAX_ANALYTICS_EVENTS_PER_REPLAY) {
2849
+ return false;
2850
+ }
2851
+ state.analyticsEventCount += 1;
2852
+ const stored = readStoredReplaySession();
2853
+ if (stored?.replayId === state.replayId) {
2854
+ writeStoredReplaySession({
2855
+ ...stored,
2856
+ analyticsEventCount: state.analyticsEventCount,
2857
+ });
2858
+ }
2859
+ return true;
2860
+ }
2824
2861
  /**
2825
2862
  * Mark a tracked analytics event on the replay timeline. Only the event name
2826
2863
  * is recorded; event properties stay out of the replay.
@@ -2829,23 +2866,64 @@ export function emitSessionReplayAnalyticsEvent(name) {
2829
2866
  const state = getState();
2830
2867
  if (!state.active || !state.addCustomEvent)
2831
2868
  return;
2832
- if (state.analyticsEventCount >= MAX_ANALYTICS_EVENTS_PER_REPLAY)
2833
- return;
2834
2869
  const bounded = name.trim().slice(0, MAX_ANALYTICS_EVENT_NAME_LENGTH);
2835
- if (!bounded)
2870
+ if (!bounded || !reserveAnalyticsMarker(state))
2836
2871
  return;
2837
- state.analyticsEventCount += 1;
2838
- const stored = readStoredReplaySession();
2839
- if (stored?.replayId === state.replayId) {
2840
- writeStoredReplaySession({
2841
- ...stored,
2842
- analyticsEventCount: state.analyticsEventCount,
2843
- });
2844
- }
2845
2872
  emitReplayCustomEvent(state, SESSION_REPLAY_ANALYTICS_EVENT_TAG, {
2846
2873
  name: bounded,
2847
2874
  });
2848
2875
  }
2876
+ /**
2877
+ * Mark a slow `action.response` on the replay timeline with that event's own
2878
+ * timing, which runs to the end of the body; a network event stops at the
2879
+ * headers. Only these operational fields are recorded, and the marker shares
2880
+ * the analytics marker budget.
2881
+ */
2882
+ export function emitSessionReplaySlowRequest(input) {
2883
+ const state = getState();
2884
+ if (!state.active || !state.addCustomEvent)
2885
+ return;
2886
+ if (typeof input.duration_ms !== "number")
2887
+ return;
2888
+ if (!reserveAnalyticsMarker(state))
2889
+ return;
2890
+ emitReplayCustomEvent(state, SESSION_REPLAY_SLOW_REQUEST_EVENT_TAG, {
2891
+ ...(typeof input.action === "string"
2892
+ ? { action: input.action.slice(0, MAX_ANALYTICS_EVENT_NAME_LENGTH) }
2893
+ : {}),
2894
+ ...(typeof input.method === "string"
2895
+ ? { method: input.method.slice(0, 10) }
2896
+ : {}),
2897
+ duration_ms: input.duration_ms,
2898
+ ...(typeof input.status_code === "number"
2899
+ ? { status_code: input.status_code }
2900
+ : {}),
2901
+ ...(typeof input.outcome === "string"
2902
+ ? { outcome: input.outcome.slice(0, 20) }
2903
+ : {}),
2904
+ page_hidden: input.page_hidden === true,
2905
+ });
2906
+ }
2907
+ /**
2908
+ * Mark a finished page view's Web Vitals on the replay timeline. Shares the
2909
+ * analytics marker budget, since each page view adds one.
2910
+ */
2911
+ export function emitSessionReplayWebVitals(input) {
2912
+ const state = getState();
2913
+ if (!state.active || !state.addCustomEvent)
2914
+ return;
2915
+ if (!reserveAnalyticsMarker(state))
2916
+ return;
2917
+ const metric = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
2918
+ emitReplayCustomEvent(state, SESSION_REPLAY_VITALS_EVENT_TAG, {
2919
+ ...(input.route ? { route: input.route.slice(0, 200) } : {}),
2920
+ navigationType: input.navigationType.slice(0, 20),
2921
+ ...(metric(input.ttfbMs) !== undefined ? { ttfbMs: input.ttfbMs } : {}),
2922
+ ...(metric(input.lcpMs) !== undefined ? { lcpMs: input.lcpMs } : {}),
2923
+ ...(metric(input.inpMs) !== undefined ? { inpMs: input.inpMs } : {}),
2924
+ ...(metric(input.cls) !== undefined ? { cls: input.cls } : {}),
2925
+ });
2926
+ }
2849
2927
  export function emitSessionReplayAgentChatEvent(input) {
2850
2928
  const state = getState();
2851
2929
  if (!state.active || !state.addCustomEvent)
@@ -1,4 +1,5 @@
1
1
  import { hashKey, useQuery, useMutation, useQueryClient, } from "@tanstack/react-query";
2
+ import { SLOW_ACTION_RESPONSE_MS } from "../shared/analytics-events.js";
2
3
  import { ANALYTICS_CLIENT_PLATFORM_HEADER } from "../shared/analytics-platform.js";
3
4
  import { actionCircuitRemainingMs, actionErrorCode, actionErrorRetryAfterMs, actionErrorStatus, assertActionCircuitClosed, endsActionPolling, isActionCircuitOpenError, isTerminalActionError, recordActionFailure, resetActionFailureCircuit, resetActionFailureCircuits, } from "./action-failure-circuit.js";
4
5
  import { getAnalyticsClientPlatform } from "./analytics-platform.js";
@@ -9,6 +10,7 @@ import { agentNativeApiDisabledReason, assertAgentNativeApiEnabled, } from "./ap
9
10
  import { getBrowserTabId } from "./browser-tab-id.js";
10
11
  import { clientBuildId, clientCompatibilityVersion, reloadForClientCompatibilityMismatch, } from "./build-compatibility.js";
11
12
  import { ensureEmbedAuthFetchInterceptor } from "./embed-auth.js";
13
+ import { currentRouteTemplate } from "./route-template.js";
12
14
  import { recheckSessionAfterUnauthorized } from "./use-session.js";
13
15
  function actionPrefix() {
14
16
  return agentNativePath("/_agent-native/actions");
@@ -427,8 +429,19 @@ function parseServerTiming(response) {
427
429
  }
428
430
  return timings;
429
431
  }
432
+ function actionTelemetryRoute() {
433
+ if (typeof window === "undefined")
434
+ return undefined;
435
+ try {
436
+ return currentRouteTemplate() ?? undefined;
437
+ }
438
+ catch {
439
+ // coercion-ok: telemetry never changes the action; the event omits route.
440
+ return undefined;
441
+ }
442
+ }
430
443
  function getActionResponseSampling(error, durationMs, response) {
431
- if (error || durationMs >= 1_000) {
444
+ if (error || durationMs >= SLOW_ACTION_RESPONSE_MS) {
432
445
  return { track: true, sampleRate: 1, sampled: false };
433
446
  }
434
447
  if (response && response.status >= 400 && response.status < 500) {
@@ -446,6 +459,7 @@ function getActionResponseSampling(error, durationMs, response) {
446
459
  async function actionFetch(name, method, params, options) {
447
460
  assertAgentNativeApiEnabled(`${method} ${name}`);
448
461
  const startedAt = actionTelemetryNow();
462
+ const routeAtStart = actionTelemetryRoute();
449
463
  const hiddenEpochAtStart = pageHiddenEpoch;
450
464
  const hiddenAtStart = typeof document !== "undefined" && document.visibilityState !== "visible";
451
465
  const timeoutMs = options?.timeoutMs ?? DEFAULT_ACTION_TIMEOUT_MS;
@@ -492,6 +506,7 @@ async function actionFetch(name, method, params, options) {
492
506
  request_id: response?.headers.get("x-agent-native-request-id") ?? undefined,
493
507
  action: name,
494
508
  method,
509
+ route: routeAtStart,
495
510
  sample_rate: sampling.sampleRate,
496
511
  sample_weight: 1 / sampling.sampleRate,
497
512
  sampled: sampling.sampled,