@agent-native/core 0.209.1 → 0.209.2-nightly-20261015213030
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/agent/actions/list-model-providers.js +15 -2
- package/dist/agent/app-model-defaults.d.ts +9 -13
- package/dist/agent/app-model-defaults.js +23 -5
- package/dist/agent/default-agent-engine.d.ts +5 -3
- package/dist/agent/default-agent-engine.js +38 -8
- package/dist/agent/durable-background.d.ts +2 -0
- package/dist/agent/durable-background.js +2 -0
- package/dist/agent/engine/registry.js +6 -0
- package/dist/agent/production-agent.js +58 -5
- package/dist/automations/service.d.ts +2 -0
- package/dist/automations/service.js +4 -1
- package/dist/client/analytics.js +48 -1
- package/dist/client/application-state.js +7 -2
- package/dist/client/chat/agentkit-protocol.js +12 -3
- package/dist/client/client-bootstrap.js +2 -0
- package/dist/client/problem-report.d.ts +11 -0
- package/dist/client/problem-report.js +19 -0
- package/dist/client/recorder.d.ts +88 -0
- package/dist/client/recorder.js +585 -0
- package/dist/client/route-chunk-recovery.js +4 -1
- package/dist/client/sign-out.js +2 -0
- package/dist/client/use-action.js +3 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/google-docs-poller.js +12 -5
- package/dist/integrations/plugin.js +86 -19
- package/dist/integrations/request-scope.d.ts +16 -0
- package/dist/integrations/request-scope.js +27 -0
- package/dist/integrations/webhook-handler.d.ts +2 -0
- package/dist/integrations/webhook-handler.js +38 -18
- package/dist/jobs/background-automation-runner.d.ts +2 -0
- package/dist/jobs/background-automation-runner.js +25 -2
- package/dist/jobs/scheduler.js +36 -22
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/org/context.d.ts +12 -0
- package/dist/org/context.js +26 -11
- package/dist/org/index.d.ts +1 -1
- package/dist/problem-report/actions/read-problem-report.d.ts +27 -0
- package/dist/problem-report/actions/read-problem-report.js +103 -0
- package/dist/problem-report/actions/save-problem-report.d.ts +35 -0
- package/dist/problem-report/actions/save-problem-report.js +36 -0
- package/dist/problem-report/format.d.ts +7 -0
- package/dist/problem-report/format.js +110 -0
- package/dist/problem-report/schema.d.ts +41 -0
- package/dist/problem-report/schema.js +34 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +9 -4
- package/dist/scripts/agent-engines/set-agent-engine.js +7 -1
- package/dist/server/action-discovery.js +11 -0
- package/dist/server/action-routes.js +2 -0
- package/dist/server/agent-chat-ai-setup.d.ts +2 -3
- package/dist/server/agent-chat-ai-setup.js +18 -4
- package/dist/server/agent-chat-plugin.js +97 -22
- package/dist/server/agent-chat-stream.d.ts +17 -5
- package/dist/server/agent-chat-stream.js +15 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-default-model-route.js +39 -6
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/agent-engine-status-cache.d.ts +4 -0
- package/dist/server/agent-engine-status-cache.js +2 -0
- package/dist/server/agent-run-context.d.ts +9 -0
- package/dist/server/agent-run-context.js +82 -17
- package/dist/server/builder-browser.js +18 -3
- package/dist/server/chatgpt-subscription-oauth.js +12 -17
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.js +46 -9
- package/dist/server/loopback.d.ts +1 -0
- package/dist/server/loopback.js +30 -0
- package/dist/server/origin-allowlist.d.ts +2 -0
- package/dist/server/origin-allowlist.js +13 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +11 -1
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
- package/dist/triggers/dispatcher.js +117 -31
- package/docs/content/recorder.mdx +259 -0
- package/docs/content/template-clips-developers.mdx +9 -8
- package/docs/content/template-clips-features.mdx +8 -7
- package/package.json +5 -3
- package/src/templates/chat/app/components/layout/Sidebar.tsx +11 -1
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type RecorderEventType = "action" | "state" | "nav" | "error" | "toast" | "log";
|
|
2
|
+
export type RecorderReportReason = "user" | "error" | "test" | "manual";
|
|
3
|
+
export interface RecorderEvent {
|
|
4
|
+
/** Seconds before the report was made (negative). */
|
|
5
|
+
t: number;
|
|
6
|
+
/** Epoch ms, the same clock as Clips diagnostics `timestampMs`. */
|
|
7
|
+
at: number;
|
|
8
|
+
type: RecorderEventType;
|
|
9
|
+
name?: string;
|
|
10
|
+
ok?: boolean;
|
|
11
|
+
status?: number;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
ms?: number;
|
|
14
|
+
requestId?: string;
|
|
15
|
+
/** Redacted and bounded: strings ≤ 200 characters, the event ≤ about 2 KB. */
|
|
16
|
+
data?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface RecorderPendingWork {
|
|
19
|
+
type: "action" | "state" | string;
|
|
20
|
+
name: string;
|
|
21
|
+
ageMs: number;
|
|
22
|
+
}
|
|
23
|
+
export interface RecorderReport {
|
|
24
|
+
app: string;
|
|
25
|
+
build: string;
|
|
26
|
+
/** The route path, with every query value redacted. */
|
|
27
|
+
url: string;
|
|
28
|
+
at: string;
|
|
29
|
+
reason: RecorderReportReason;
|
|
30
|
+
windowSeconds: number;
|
|
31
|
+
events: RecorderEvent[];
|
|
32
|
+
watched: Record<string, unknown>;
|
|
33
|
+
pending: RecorderPendingWork[];
|
|
34
|
+
/** Events lost to the caps since the last clear. */
|
|
35
|
+
dropped: number;
|
|
36
|
+
}
|
|
37
|
+
export interface RecorderReportOptions {
|
|
38
|
+
seconds?: number;
|
|
39
|
+
reason?: RecorderReportReason;
|
|
40
|
+
}
|
|
41
|
+
/** @internal Called when an action starts; call the result when it settles. */
|
|
42
|
+
export declare function recordActionStart(name: string, method: string): (outcome: {
|
|
43
|
+
error: unknown;
|
|
44
|
+
response: Response | undefined;
|
|
45
|
+
signal: AbortSignal | undefined;
|
|
46
|
+
}) => void;
|
|
47
|
+
/** @internal An `application_state` write, or a delete when `value` is undefined. */
|
|
48
|
+
export declare function recordStateWrite(key: string, value: unknown, write: Promise<unknown>, signal: AbortSignal | undefined): void;
|
|
49
|
+
/** @internal */
|
|
50
|
+
export declare function recordNav(): void;
|
|
51
|
+
/** @internal The report without notifying `onReport` listeners, for core's own destinations. */
|
|
52
|
+
export declare function buildRecorderReport(reason: RecorderReportReason, seconds?: number): RecorderReport;
|
|
53
|
+
declare function report(options?: RecorderReportOptions): RecorderReport;
|
|
54
|
+
declare function pending(): RecorderPendingWork[];
|
|
55
|
+
export declare const recorder: {
|
|
56
|
+
/** Synchronous. Listeners hear about every report except a `"manual"` one. */
|
|
57
|
+
report: typeof report;
|
|
58
|
+
log(name: string, data?: unknown): void;
|
|
59
|
+
/** Adds `getValue()` to every report. Returns a function that stops it. */
|
|
60
|
+
watch(name: string, getValue: () => unknown): () => void;
|
|
61
|
+
onReport(listener: (report: RecorderReport) => void): () => void;
|
|
62
|
+
/** Field names (`"body"`), or dotted paths that start with the event's name (`"compose.body"`, `"payment.*"`). */
|
|
63
|
+
redact(...keys: string[]): void;
|
|
64
|
+
pending: typeof pending;
|
|
65
|
+
disable(): void;
|
|
66
|
+
};
|
|
67
|
+
declare global {
|
|
68
|
+
interface Window {
|
|
69
|
+
__agentNativeRecorder?: {
|
|
70
|
+
report: typeof report;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export declare function configureRecorder(options: {
|
|
75
|
+
windowSeconds?: number;
|
|
76
|
+
maxEvents?: number;
|
|
77
|
+
sentry?: boolean;
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
}): void;
|
|
80
|
+
/** @internal Whether `ensureSentry()` should attach recorder events. */
|
|
81
|
+
export declare function recorderSentryEnabled(): boolean;
|
|
82
|
+
/** @internal Forgets everything recorded, e.g. on sign-out. */
|
|
83
|
+
export declare function clearRecorder(): void;
|
|
84
|
+
/** @internal Called once from client bootstrap. */
|
|
85
|
+
export declare function startRecorder(): void;
|
|
86
|
+
/** @internal */
|
|
87
|
+
export declare function resetRecorderForTests(): void;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { redactCapturedString } from "../observability/trace-redaction.js";
|
|
2
|
+
import { redact, SECRET_KEY_RE } from "../tracking/redaction.js";
|
|
3
|
+
import { actionErrorCode, actionErrorStatus, } from "./action-failure-circuit.js";
|
|
4
|
+
import { injectedAgentNativeAppId } from "./app-config.js";
|
|
5
|
+
import { clientBuildId } from "./build-compatibility.js";
|
|
6
|
+
import { normalizeCapturedError } from "./error-capture.js";
|
|
7
|
+
const MAX_STRING_LENGTH = 200;
|
|
8
|
+
const MAX_DATA_CHARS = 1_600;
|
|
9
|
+
const MAX_WATCHED_CHARS = 4_096;
|
|
10
|
+
// Bounds the tab's memory whatever maxEvents is set to.
|
|
11
|
+
const MAX_BUFFER_CHARS = 512 * 1024;
|
|
12
|
+
const MAX_STATE_SNAPSHOT_CHARS = 16 * 1024;
|
|
13
|
+
const MAX_STATE_BASELINES = 16;
|
|
14
|
+
const MAX_DEPTH = 6;
|
|
15
|
+
const ERROR_REPORT_INTERVAL_MS = 10_000;
|
|
16
|
+
const REQUEST_ID_HEADER = "x-agent-native-request-id";
|
|
17
|
+
const REDACTED = "[redacted]";
|
|
18
|
+
const REMOVED = "[removed]";
|
|
19
|
+
const SECRET_KEY = new RegExp(`${SECRET_KEY_RE.source}|session|card|cvv`, "i");
|
|
20
|
+
const TOAST_SELECTOR = "[data-sonner-toast]";
|
|
21
|
+
// Sonner mounts this list with the first toast, so other inserted DOM is never scanned.
|
|
22
|
+
const TOASTER_SELECTOR = "[data-sonner-toaster]";
|
|
23
|
+
let windowSeconds = 120;
|
|
24
|
+
let maxEvents = 2_000;
|
|
25
|
+
let sentry = true;
|
|
26
|
+
let enabled = true;
|
|
27
|
+
let started = false;
|
|
28
|
+
let buffer = [];
|
|
29
|
+
let bufferChars = 0;
|
|
30
|
+
let dropped = 0;
|
|
31
|
+
let recordingSince = Date.now();
|
|
32
|
+
// Bumped by every clear, so work that started before a sign-out records nothing.
|
|
33
|
+
let generation = 0;
|
|
34
|
+
let lastPath = null;
|
|
35
|
+
let lastErrorReportAt = -Infinity;
|
|
36
|
+
let nextWorkId = 0;
|
|
37
|
+
let toastObserver = null;
|
|
38
|
+
const inFlight = new Map();
|
|
39
|
+
// The last value each key saved, so a write records only what it changed.
|
|
40
|
+
const stateBaselines = new Map();
|
|
41
|
+
const watchers = new Map();
|
|
42
|
+
const listeners = new Set();
|
|
43
|
+
const extraRedactions = [];
|
|
44
|
+
function recording() {
|
|
45
|
+
return enabled && typeof window !== "undefined";
|
|
46
|
+
}
|
|
47
|
+
function record(build) {
|
|
48
|
+
if (!recording())
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
push(build());
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// The app never sees a recorder failure; the lost event is still counted.
|
|
55
|
+
dropped += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function push(event) {
|
|
59
|
+
const stored = {
|
|
60
|
+
at: Date.now(),
|
|
61
|
+
...event,
|
|
62
|
+
...(event.name === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: { name: cleanString(event.name, true) }),
|
|
65
|
+
...(event.data === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: {
|
|
68
|
+
data: clean(event.data, redactionRoot(event.name), { left: MAX_DATA_CHARS }, 0, true),
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
const chars = JSON.stringify(stored).length;
|
|
72
|
+
buffer.push({ event: stored, chars });
|
|
73
|
+
bufferChars += chars;
|
|
74
|
+
trim(stored.at);
|
|
75
|
+
}
|
|
76
|
+
function trim(now) {
|
|
77
|
+
const oldest = now - windowSeconds * 1000;
|
|
78
|
+
while (buffer.length > 0 && buffer[0].event.at < oldest) {
|
|
79
|
+
bufferChars -= buffer.shift().chars;
|
|
80
|
+
}
|
|
81
|
+
while (buffer.length > maxEvents || bufferChars > MAX_BUFFER_CHARS) {
|
|
82
|
+
bufferChars -= buffer.shift().chars;
|
|
83
|
+
dropped += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function redactText(text, truncated) {
|
|
87
|
+
const masked = redactCapturedString(redact(text), { truncated });
|
|
88
|
+
return masked === text
|
|
89
|
+
? text
|
|
90
|
+
: masked.replace(/<redacted>|\[REDACTED\]/g, REDACTED);
|
|
91
|
+
}
|
|
92
|
+
function cleanString(value, redactSecrets, budget) {
|
|
93
|
+
const cut = value.length > MAX_STRING_LENGTH;
|
|
94
|
+
let text = cut ? value.slice(0, MAX_STRING_LENGTH) : value;
|
|
95
|
+
if (redactSecrets)
|
|
96
|
+
text = redactText(text, cut);
|
|
97
|
+
if (cut)
|
|
98
|
+
text += "…";
|
|
99
|
+
if (budget)
|
|
100
|
+
budget.left -= text.length + 2;
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
function isPlainObject(value) {
|
|
104
|
+
if (value === null || typeof value !== "object")
|
|
105
|
+
return false;
|
|
106
|
+
const prototype = Object.getPrototypeOf(value);
|
|
107
|
+
return prototype === Object.prototype || prototype === null;
|
|
108
|
+
}
|
|
109
|
+
function redactionRoot(name) {
|
|
110
|
+
return extraRedactions.length > 0 ? (name ?? "") : null;
|
|
111
|
+
}
|
|
112
|
+
function isRedactedField(key, path) {
|
|
113
|
+
if (SECRET_KEY.test(key))
|
|
114
|
+
return true;
|
|
115
|
+
if (path === null)
|
|
116
|
+
return false;
|
|
117
|
+
return extraRedactions.some((rule) => "field" in rule ? rule.field === key : rule.path.test(path));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A JSON-safe copy with secret fields redacted and strings and size capped.
|
|
121
|
+
* What does not fit is replaced by a marker that says what was cut.
|
|
122
|
+
*/
|
|
123
|
+
function clean(value, path, budget, depth, redactSecrets) {
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
return cleanString(value, redactSecrets, budget);
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === "number") {
|
|
128
|
+
budget.left -= 8;
|
|
129
|
+
return Number.isFinite(value) ? value : String(value);
|
|
130
|
+
}
|
|
131
|
+
if (typeof value === "boolean" || value === null) {
|
|
132
|
+
budget.left -= 5;
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "bigint")
|
|
136
|
+
return cleanString(String(value), false, budget);
|
|
137
|
+
if (typeof value !== "object")
|
|
138
|
+
return undefined;
|
|
139
|
+
if (value instanceof Error) {
|
|
140
|
+
return cleanString(`${value.name}: ${value.message}`, redactSecrets, budget);
|
|
141
|
+
}
|
|
142
|
+
if (typeof value.toJSON === "function") {
|
|
143
|
+
return clean(value.toJSON(), path, budget, depth + 1, redactSecrets);
|
|
144
|
+
}
|
|
145
|
+
if (depth >= MAX_DEPTH) {
|
|
146
|
+
budget.left -= 5;
|
|
147
|
+
return "[…]";
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
const out = [];
|
|
151
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
152
|
+
if (budget.left <= 0) {
|
|
153
|
+
out.push(`[+${value.length - i} more]`);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
const item = clean(value[i], path === null ? null : `${path}.${i}`, budget, depth + 1, redactSecrets);
|
|
157
|
+
out.push(item === undefined ? null : item);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
if (!isPlainObject(value)) {
|
|
162
|
+
return cleanString(`[${value.constructor?.name ?? "object"}]`, false, budget);
|
|
163
|
+
}
|
|
164
|
+
const out = {};
|
|
165
|
+
const keys = Object.keys(value);
|
|
166
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
167
|
+
const key = keys[i];
|
|
168
|
+
if (budget.left <= 0) {
|
|
169
|
+
out["…"] = `+${keys.length - i} more keys`;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
budget.left -= key.length + 4;
|
|
173
|
+
const childPath = path === null ? null : `${path}.${key}`;
|
|
174
|
+
if (isRedactedField(key, childPath)) {
|
|
175
|
+
out[key] = REDACTED;
|
|
176
|
+
budget.left -= REDACTED.length;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const child = clean(value[key], childPath, budget, depth + 1, redactSecrets);
|
|
180
|
+
if (child !== undefined)
|
|
181
|
+
out[key] = child;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function sameValue(previous, next) {
|
|
186
|
+
if (previous === next)
|
|
187
|
+
return true;
|
|
188
|
+
return (typeof previous === "object" &&
|
|
189
|
+
typeof next === "object" &&
|
|
190
|
+
JSON.stringify(previous) === JSON.stringify(next));
|
|
191
|
+
}
|
|
192
|
+
/** Only the leaves that differ, or `undefined` when nothing did. */
|
|
193
|
+
function changes(previous, next) {
|
|
194
|
+
if (!isPlainObject(previous) || !isPlainObject(next)) {
|
|
195
|
+
return sameValue(previous, next) ? undefined : next;
|
|
196
|
+
}
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const key of Object.keys(next)) {
|
|
199
|
+
const change = changes(previous[key], next[key]);
|
|
200
|
+
if (change !== undefined)
|
|
201
|
+
out[key] = change;
|
|
202
|
+
}
|
|
203
|
+
for (const key of Object.keys(previous)) {
|
|
204
|
+
if (!(key in next))
|
|
205
|
+
out[key] = REMOVED;
|
|
206
|
+
}
|
|
207
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
208
|
+
}
|
|
209
|
+
function rememberBaseline(key, value) {
|
|
210
|
+
stateBaselines.delete(key);
|
|
211
|
+
stateBaselines.set(key, value);
|
|
212
|
+
if (stateBaselines.size > MAX_STATE_BASELINES) {
|
|
213
|
+
stateBaselines.delete(stateBaselines.keys().next().value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function stateChange(key, value, saved) {
|
|
217
|
+
// Secrets are kept out of the snapshot; other text is redacted when the change is recorded.
|
|
218
|
+
const next = clean(value, redactionRoot(key), { left: MAX_STATE_SNAPSHOT_CHARS }, 0, false);
|
|
219
|
+
const hadBaseline = stateBaselines.has(key);
|
|
220
|
+
const change = hadBaseline ? changes(stateBaselines.get(key), next) : next;
|
|
221
|
+
if (saved)
|
|
222
|
+
rememberBaseline(key, next);
|
|
223
|
+
if (!hadBaseline)
|
|
224
|
+
return { value: change };
|
|
225
|
+
return change === undefined ? { unchanged: true } : { changed: change };
|
|
226
|
+
}
|
|
227
|
+
function errorMessage(error) {
|
|
228
|
+
return normalizeCapturedError(error).message;
|
|
229
|
+
}
|
|
230
|
+
function stringField(value, key) {
|
|
231
|
+
const field = value && typeof value === "object"
|
|
232
|
+
? value[key]
|
|
233
|
+
: undefined;
|
|
234
|
+
return typeof field === "string" ? field : undefined;
|
|
235
|
+
}
|
|
236
|
+
function trackWork(type, name, since) {
|
|
237
|
+
const id = (nextWorkId += 1);
|
|
238
|
+
inFlight.set(id, { type, name, since });
|
|
239
|
+
return id;
|
|
240
|
+
}
|
|
241
|
+
/** @internal Called when an action starts; call the result when it settles. */
|
|
242
|
+
export function recordActionStart(name, method) {
|
|
243
|
+
if (!recording())
|
|
244
|
+
return () => { };
|
|
245
|
+
const startedAt = Date.now();
|
|
246
|
+
const startedIn = generation;
|
|
247
|
+
const id = trackWork("action", name, startedAt);
|
|
248
|
+
return ({ error, response, signal }) => {
|
|
249
|
+
inFlight.delete(id);
|
|
250
|
+
if (generation !== startedIn)
|
|
251
|
+
return;
|
|
252
|
+
record(() => {
|
|
253
|
+
const cancelled = Boolean(error) && signal?.aborted === true;
|
|
254
|
+
const status = response?.status ?? actionErrorStatus(error);
|
|
255
|
+
const errorCode = cancelled
|
|
256
|
+
? "cancelled"
|
|
257
|
+
: (actionErrorCode(error) ?? stringField(error, "code"));
|
|
258
|
+
const requestId = response?.headers.get(REQUEST_ID_HEADER) ??
|
|
259
|
+
stringField(error, "requestId");
|
|
260
|
+
return {
|
|
261
|
+
type: "action",
|
|
262
|
+
name,
|
|
263
|
+
ok: !error,
|
|
264
|
+
...(status === undefined ? {} : { status }),
|
|
265
|
+
...(errorCode === undefined ? {} : { errorCode }),
|
|
266
|
+
ms: Date.now() - startedAt,
|
|
267
|
+
...(requestId ? { requestId } : {}),
|
|
268
|
+
data: error && !cancelled
|
|
269
|
+
? { method, error: errorMessage(error) }
|
|
270
|
+
: { method },
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** @internal An `application_state` write, or a delete when `value` is undefined. */
|
|
276
|
+
export function recordStateWrite(key, value, write, signal) {
|
|
277
|
+
if (!recording())
|
|
278
|
+
return;
|
|
279
|
+
const startedAt = Date.now();
|
|
280
|
+
const startedIn = generation;
|
|
281
|
+
const id = trackWork("state", key, startedAt);
|
|
282
|
+
const settle = (ok, error) => {
|
|
283
|
+
inFlight.delete(id);
|
|
284
|
+
if (generation !== startedIn)
|
|
285
|
+
return;
|
|
286
|
+
record(() => {
|
|
287
|
+
const cancelled = !ok && signal?.aborted === true;
|
|
288
|
+
const status = ok ? undefined : actionErrorStatus(error);
|
|
289
|
+
const data = value === undefined ? { deleted: true } : stateChange(key, value, ok);
|
|
290
|
+
if (value === undefined && ok)
|
|
291
|
+
stateBaselines.delete(key);
|
|
292
|
+
if (!ok && !cancelled)
|
|
293
|
+
data.error = errorMessage(error);
|
|
294
|
+
return {
|
|
295
|
+
type: "state",
|
|
296
|
+
name: key,
|
|
297
|
+
ok,
|
|
298
|
+
...(status === undefined ? {} : { status }),
|
|
299
|
+
...(cancelled ? { errorCode: "cancelled" } : {}),
|
|
300
|
+
ms: Date.now() - startedAt,
|
|
301
|
+
data,
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
write.then(() => settle(true), (error) => settle(false, error));
|
|
306
|
+
}
|
|
307
|
+
/** @internal */
|
|
308
|
+
export function recordNav() {
|
|
309
|
+
if (!recording())
|
|
310
|
+
return;
|
|
311
|
+
const path = window.location.pathname;
|
|
312
|
+
if (path === lastPath)
|
|
313
|
+
return;
|
|
314
|
+
lastPath = path;
|
|
315
|
+
record(() => ({ type: "nav", name: path }));
|
|
316
|
+
}
|
|
317
|
+
function topFrame(stack) {
|
|
318
|
+
for (const line of stack?.split("\n") ?? []) {
|
|
319
|
+
const match = /(?:^|[\s(@])((?:[a-z][\w+.-]*:\/\/|\/)[^\s()]*):(\d+):\d+\)?\s*$/i.exec(line);
|
|
320
|
+
if (!match)
|
|
321
|
+
continue;
|
|
322
|
+
const file = match[1]
|
|
323
|
+
.replace(/^[a-z][\w+.-]*:\/\/[^/]*/i, "")
|
|
324
|
+
.replace(/[?#].*$/, "");
|
|
325
|
+
return `${file}:${match[2]}`;
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
function recordError(error, kind) {
|
|
330
|
+
record(() => {
|
|
331
|
+
const { type, message, stack } = normalizeCapturedError(error);
|
|
332
|
+
const source = topFrame(stack);
|
|
333
|
+
return {
|
|
334
|
+
type: "error",
|
|
335
|
+
name: type,
|
|
336
|
+
data: { kind, message, ...(source ? { source } : {}) },
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
// An error loop would otherwise build and send a full report per error.
|
|
341
|
+
if (listeners.size > 0 &&
|
|
342
|
+
now - lastErrorReportAt >= ERROR_REPORT_INTERVAL_MS) {
|
|
343
|
+
lastErrorReportAt = now;
|
|
344
|
+
report({ reason: "error" });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function onWindowError(event) {
|
|
348
|
+
recordError(event.error ?? event.message, "error");
|
|
349
|
+
}
|
|
350
|
+
function onUnhandledRejection(event) {
|
|
351
|
+
recordError(event.reason, "unhandledrejection");
|
|
352
|
+
}
|
|
353
|
+
function observeToasts() {
|
|
354
|
+
const recorded = new WeakMap();
|
|
355
|
+
const check = (toast) => {
|
|
356
|
+
const level = toast.getAttribute("data-type");
|
|
357
|
+
if (level !== "error" && level !== "warning")
|
|
358
|
+
return;
|
|
359
|
+
const text = (toast.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
360
|
+
if (!text || recorded.get(toast) === `${level}:${text}`)
|
|
361
|
+
return;
|
|
362
|
+
recorded.set(toast, `${level}:${text}`);
|
|
363
|
+
record(() => ({ type: "toast", name: level, data: { text } }));
|
|
364
|
+
};
|
|
365
|
+
const observer = new MutationObserver((mutations) => {
|
|
366
|
+
for (const mutation of mutations) {
|
|
367
|
+
if (mutation.type === "attributes") {
|
|
368
|
+
const target = mutation.target;
|
|
369
|
+
if (target.matches(TOAST_SELECTOR))
|
|
370
|
+
check(target);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
for (const node of mutation.addedNodes) {
|
|
374
|
+
if (!(node instanceof Element))
|
|
375
|
+
continue;
|
|
376
|
+
if (node.matches(TOAST_SELECTOR))
|
|
377
|
+
check(node);
|
|
378
|
+
else if (node.matches(TOASTER_SELECTOR)) {
|
|
379
|
+
node.querySelectorAll(TOAST_SELECTOR).forEach(check);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
observer.observe(document.documentElement, {
|
|
385
|
+
childList: true,
|
|
386
|
+
subtree: true,
|
|
387
|
+
attributes: true,
|
|
388
|
+
attributeFilter: ["data-type"],
|
|
389
|
+
});
|
|
390
|
+
return observer;
|
|
391
|
+
}
|
|
392
|
+
function reportUrl() {
|
|
393
|
+
const { pathname, search } = window.location;
|
|
394
|
+
const keys = [...new Set(new URLSearchParams(search).keys())];
|
|
395
|
+
const query = keys
|
|
396
|
+
.map((key) => `${encodeURIComponent(key)}=${REDACTED}`)
|
|
397
|
+
.join("&");
|
|
398
|
+
return cleanString(pathname, true) + (query ? `?${query}` : "");
|
|
399
|
+
}
|
|
400
|
+
/** @internal The report without notifying `onReport` listeners, for core's own destinations. */
|
|
401
|
+
export function buildRecorderReport(reason, seconds) {
|
|
402
|
+
if (seconds !== undefined && !(seconds >= 0)) {
|
|
403
|
+
throw new RangeError("recorder.report({ seconds }) needs a number of seconds of 0 or more.");
|
|
404
|
+
}
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
trim(now);
|
|
407
|
+
const requested = seconds === undefined ? windowSeconds : Math.min(seconds, windowSeconds);
|
|
408
|
+
const from = now - requested * 1000;
|
|
409
|
+
const events = [];
|
|
410
|
+
for (const { event } of buffer) {
|
|
411
|
+
if (event.at >= from) {
|
|
412
|
+
events.push({ t: (event.at - now) / 1000, ...event });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const watched = {};
|
|
416
|
+
if (enabled) {
|
|
417
|
+
for (const [name, getValue] of watchers) {
|
|
418
|
+
try {
|
|
419
|
+
watched[name] = clean(getValue(), redactionRoot(name), { left: MAX_WATCHED_CHARS }, 0, true);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
watched[name] = { unreadable: cleanString(errorMessage(error), true) };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
app: injectedAgentNativeAppId() ?? (window.location.host || "unknown"),
|
|
428
|
+
build: clientBuildId() || "unknown",
|
|
429
|
+
url: reportUrl(),
|
|
430
|
+
at: new Date(now).toISOString(),
|
|
431
|
+
reason,
|
|
432
|
+
windowSeconds: Math.min(requested, Math.max(0, Math.ceil((now - recordingSince) / 1000))),
|
|
433
|
+
events,
|
|
434
|
+
watched,
|
|
435
|
+
pending: pending(),
|
|
436
|
+
dropped,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function report(options = {}) {
|
|
440
|
+
const built = buildRecorderReport(options.reason ?? "manual", options.seconds);
|
|
441
|
+
if (built.reason === "manual" || !enabled)
|
|
442
|
+
return built;
|
|
443
|
+
for (const listener of listeners) {
|
|
444
|
+
try {
|
|
445
|
+
listener(built);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.error("[agent-native] A recorder onReport listener failed.", error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return built;
|
|
452
|
+
}
|
|
453
|
+
function pending() {
|
|
454
|
+
const now = Date.now();
|
|
455
|
+
return [...inFlight.values()].map(({ type, name, since }) => ({
|
|
456
|
+
type,
|
|
457
|
+
name,
|
|
458
|
+
ageMs: now - since,
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
function redactPattern(path) {
|
|
462
|
+
if (!path.includes("."))
|
|
463
|
+
return { field: path };
|
|
464
|
+
const segments = path
|
|
465
|
+
.split(".")
|
|
466
|
+
.map((segment) => segment === "*"
|
|
467
|
+
? "[^.]+"
|
|
468
|
+
: segment.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"));
|
|
469
|
+
return { path: new RegExp(`^${segments.join("\\.")}$`) };
|
|
470
|
+
}
|
|
471
|
+
export const recorder = {
|
|
472
|
+
/** Synchronous. Listeners hear about every report except a `"manual"` one. */
|
|
473
|
+
report,
|
|
474
|
+
log(name, data) {
|
|
475
|
+
record(() => ({
|
|
476
|
+
type: "log",
|
|
477
|
+
name,
|
|
478
|
+
...(data === undefined
|
|
479
|
+
? {}
|
|
480
|
+
: { data: isPlainObject(data) ? data : { value: data } }),
|
|
481
|
+
}));
|
|
482
|
+
},
|
|
483
|
+
/** Adds `getValue()` to every report. Returns a function that stops it. */
|
|
484
|
+
watch(name, getValue) {
|
|
485
|
+
watchers.set(name, getValue);
|
|
486
|
+
return () => {
|
|
487
|
+
if (watchers.get(name) === getValue)
|
|
488
|
+
watchers.delete(name);
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
onReport(listener) {
|
|
492
|
+
listeners.add(listener);
|
|
493
|
+
return () => {
|
|
494
|
+
listeners.delete(listener);
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
/** Field names (`"body"`), or dotted paths that start with the event's name (`"compose.body"`, `"payment.*"`). */
|
|
498
|
+
redact(...keys) {
|
|
499
|
+
for (const key of keys)
|
|
500
|
+
extraRedactions.push(redactPattern(key));
|
|
501
|
+
},
|
|
502
|
+
pending,
|
|
503
|
+
disable() {
|
|
504
|
+
configureRecorder({ enabled: false });
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
export function configureRecorder(options) {
|
|
508
|
+
if (options.windowSeconds !== undefined) {
|
|
509
|
+
if (!(Number.isFinite(options.windowSeconds) && options.windowSeconds > 0)) {
|
|
510
|
+
throw new RangeError("configureRecorder({ windowSeconds }) needs a positive number of seconds.");
|
|
511
|
+
}
|
|
512
|
+
windowSeconds = options.windowSeconds;
|
|
513
|
+
}
|
|
514
|
+
if (options.maxEvents !== undefined) {
|
|
515
|
+
if (!(Number.isInteger(options.maxEvents) && options.maxEvents > 0)) {
|
|
516
|
+
throw new RangeError("configureRecorder({ maxEvents }) needs a positive whole number.");
|
|
517
|
+
}
|
|
518
|
+
maxEvents = options.maxEvents;
|
|
519
|
+
}
|
|
520
|
+
if (options.sentry !== undefined)
|
|
521
|
+
sentry = options.sentry;
|
|
522
|
+
if (options.enabled === false)
|
|
523
|
+
stopRecorder();
|
|
524
|
+
if (options.enabled === true) {
|
|
525
|
+
enabled = true;
|
|
526
|
+
startRecorder();
|
|
527
|
+
}
|
|
528
|
+
trim(Date.now());
|
|
529
|
+
}
|
|
530
|
+
/** @internal Whether `ensureSentry()` should attach recorder events. */
|
|
531
|
+
export function recorderSentryEnabled() {
|
|
532
|
+
return enabled && sentry;
|
|
533
|
+
}
|
|
534
|
+
/** @internal Forgets everything recorded, e.g. on sign-out. */
|
|
535
|
+
export function clearRecorder() {
|
|
536
|
+
buffer = [];
|
|
537
|
+
bufferChars = 0;
|
|
538
|
+
dropped = 0;
|
|
539
|
+
recordingSince = Date.now();
|
|
540
|
+
generation += 1;
|
|
541
|
+
inFlight.clear();
|
|
542
|
+
lastPath = null;
|
|
543
|
+
stateBaselines.clear();
|
|
544
|
+
}
|
|
545
|
+
/** @internal Called once from client bootstrap. */
|
|
546
|
+
export function startRecorder() {
|
|
547
|
+
if (started ||
|
|
548
|
+
!recording() ||
|
|
549
|
+
typeof window.addEventListener !== "function") {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
started = true;
|
|
553
|
+
window.addEventListener("error", onWindowError);
|
|
554
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
555
|
+
window.addEventListener("popstate", recordNav);
|
|
556
|
+
if (typeof MutationObserver === "function")
|
|
557
|
+
toastObserver = observeToasts();
|
|
558
|
+
window.__agentNativeRecorder = { report };
|
|
559
|
+
recordNav();
|
|
560
|
+
}
|
|
561
|
+
function stopRecorder() {
|
|
562
|
+
enabled = false;
|
|
563
|
+
clearRecorder();
|
|
564
|
+
toastObserver?.disconnect();
|
|
565
|
+
toastObserver = null;
|
|
566
|
+
if (!started)
|
|
567
|
+
return;
|
|
568
|
+
started = false;
|
|
569
|
+
window.removeEventListener("error", onWindowError);
|
|
570
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
571
|
+
window.removeEventListener("popstate", recordNav);
|
|
572
|
+
delete window.__agentNativeRecorder;
|
|
573
|
+
}
|
|
574
|
+
/** @internal */
|
|
575
|
+
export function resetRecorderForTests() {
|
|
576
|
+
stopRecorder();
|
|
577
|
+
enabled = true;
|
|
578
|
+
windowSeconds = 120;
|
|
579
|
+
maxEvents = 2_000;
|
|
580
|
+
sentry = true;
|
|
581
|
+
lastErrorReportAt = -Infinity;
|
|
582
|
+
watchers.clear();
|
|
583
|
+
listeners.clear();
|
|
584
|
+
extraRedactions.length = 0;
|
|
585
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CHUNK_RECOVERY_CACHE_BUSTER_PARAM, CHUNK_RECOVERY_ORIGINAL_HASH_PARAM, CHUNK_RECOVERY_PATH_SUFFIX, CHUNK_RECOVERY_QUERY_PARAM, CHUNK_RECOVERY_QUERY_VALUE, ROUTE_WARMUP_PRELOAD_ATTRIBUTE, STALE_CHUNK_RELOAD_AT_KEY, STALE_CHUNK_RELOAD_COOLDOWN_MS, } from "../shared/route-chunk-recovery-bootstrap.js";
|
|
2
|
+
import { recordNav } from "./recorder.js";
|
|
2
3
|
const INSTALL_KEY = "__agentNativeRouteChunkRecoveryInstalled";
|
|
3
4
|
const INTENDED_NAV_MAX_AGE_MS = 15_000;
|
|
4
5
|
const STALE_CHUNK_EXHAUSTED_KEY = "__agentNativeStaleChunkRecoveryExhausted";
|
|
@@ -332,7 +333,9 @@ function patchHistoryMethod(win, state, method) {
|
|
|
332
333
|
if (href)
|
|
333
334
|
rememberIntendedNavigation(state, href);
|
|
334
335
|
}
|
|
335
|
-
|
|
336
|
+
const result = original.apply(this, args);
|
|
337
|
+
recordNav();
|
|
338
|
+
return result;
|
|
336
339
|
};
|
|
337
340
|
}
|
|
338
341
|
function patchReload(win, state) {
|
package/dist/client/sign-out.js
CHANGED
|
@@ -24,6 +24,7 @@ import { BETA_REDIRECT_SIGN_OUT_STORAGE_KEY, BETA_REDIRECT_STORAGE_KEY, } from "
|
|
|
24
24
|
*/
|
|
25
25
|
import { clearActionQueryCache } from "./action-query-cache.js";
|
|
26
26
|
import { agentNativePath } from "./api-path.js";
|
|
27
|
+
import { clearRecorder } from "./recorder.js";
|
|
27
28
|
import { buildSignInReturnHref } from "./sign-in-return.js";
|
|
28
29
|
import { beginSignOut, completeSignOut } from "./use-session.js";
|
|
29
30
|
const LOGOUT_PATH = "/_agent-native/auth/logout";
|
|
@@ -74,6 +75,7 @@ export function signOut(options = {}) {
|
|
|
74
75
|
async function signOutFlow(options) {
|
|
75
76
|
setBetaRedirectSignOutSignal();
|
|
76
77
|
beginSignOut();
|
|
78
|
+
clearRecorder();
|
|
77
79
|
const controller = new AbortController();
|
|
78
80
|
const timeout = setTimeout(() => controller.abort(), SIGN_OUT_REQUEST_TIMEOUT_MS);
|
|
79
81
|
let revoked = false;
|