@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.
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/analytics.d.ts +1 -0
- package/dist/client/analytics.js +61 -5
- package/dist/client/route-template.d.ts +16 -0
- package/dist/client/route-template.js +127 -0
- package/dist/client/session-replay.d.ts +30 -0
- package/dist/client/session-replay.js +95 -17
- package/dist/client/use-action.js +16 -1
- package/dist/client/web-vitals.d.ts +69 -0
- package/dist/client/web-vitals.js +288 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.js +174 -36
- package/dist/mcp/embed-app.js +1 -1
- package/dist/mcp/server.js +3 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/shared/analytics-events.d.ts +14 -0
- package/dist/shared/analytics-events.js +13 -0
- package/docs/content/template-clips-features.mdx +10 -6
- package/docs/content/template-mail-inbox.mdx +2 -2
- package/docs/content/template-slides-features.mdx +1 -0
- package/package.json +4 -4
|
@@ -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
|
};
|
package/dist/client/analytics.js
CHANGED
|
@@ -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
|
-
|
|
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 >=
|
|
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,
|