@rrwebcloud/openclaw-session-recording 2026.3.28-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +5 -0
- package/README.md +29 -0
- package/extension/background.js +7 -0
- package/extension/content-script.js +46 -0
- package/extension/manifest.json +18 -0
- package/index.ts +662 -0
- package/openclaw.plugin.json +71 -0
- package/package.json +44 -0
- package/skills/rrweb-replay/SKILL.md +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# `@rrwebcloud/openclaw-session-recording`
|
|
2
|
+
|
|
3
|
+
Portable rrweb replay plugin for OpenClaw-managed browsers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @rrwebcloud/openclaw-session-recording
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Minimal config
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
plugins:
|
|
15
|
+
entries:
|
|
16
|
+
rrweb-replay:
|
|
17
|
+
enabled: true
|
|
18
|
+
publicKey: pk_live_your_public_key
|
|
19
|
+
extensionMode: bundled
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
|
|
24
|
+
- OpenClaw `2026.3.24+` uses the full automatic browser-runtime integration path
|
|
25
|
+
- OpenClaw `2026.3.13` loads in legacy compatibility mode and reports clear next steps instead of failing on install
|
|
26
|
+
- `serverUrl` defaults to `https://api.rrwebcloud.com`
|
|
27
|
+
- `secretKey` is not required for the bundled browser flow
|
|
28
|
+
- the plugin targets managed Chromium browser profiles such as `openclaw`
|
|
29
|
+
- `replay_session_info` exposes the active replay metadata
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
2
|
+
const sessionStore = chrome.storage && chrome.storage.session;
|
|
3
|
+
if (!sessionStore || typeof sessionStore.set !== "function") {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
void sessionStore.set({ openclawRrwebBootstrapInstalled: true }).catch(() => {});
|
|
7
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
(function bootstrapOpenClawReplay() {
|
|
2
|
+
const STATE_KEY = "openclawRrwebReplayContext";
|
|
3
|
+
|
|
4
|
+
async function persist(detail) {
|
|
5
|
+
if (!detail || typeof detail !== "object") {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const payload = {
|
|
9
|
+
...detail,
|
|
10
|
+
href: window.location.href,
|
|
11
|
+
capturedAt: new Date().toISOString(),
|
|
12
|
+
};
|
|
13
|
+
try {
|
|
14
|
+
if (
|
|
15
|
+
chrome.storage &&
|
|
16
|
+
chrome.storage.session &&
|
|
17
|
+
typeof chrome.storage.session.set === "function"
|
|
18
|
+
) {
|
|
19
|
+
await chrome.storage.session.set({ [STATE_KEY]: payload });
|
|
20
|
+
} else {
|
|
21
|
+
throw new Error("session storage unavailable");
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
try {
|
|
25
|
+
await chrome.storage.local.set({ [STATE_KEY]: payload });
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore storage failures
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
document.documentElement.dataset.openclawRrwebReplaySessionId =
|
|
32
|
+
typeof payload.replaySessionId === "string" ? payload.replaySessionId : "";
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore DOM updates
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const current = globalThis.__OPENCLAW_RRWEB_REPLAY;
|
|
39
|
+
if (current) {
|
|
40
|
+
void persist(current);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
window.addEventListener("openclaw:rrweb-replay-context", (event) => {
|
|
44
|
+
void persist(event && typeof event === "object" ? event.detail : undefined);
|
|
45
|
+
});
|
|
46
|
+
})();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "OpenClaw rrweb Replay Bootstrap",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Bootstrap extension for OpenClaw-managed rrweb replay context.",
|
|
6
|
+
"permissions": ["storage"],
|
|
7
|
+
"host_permissions": ["<all_urls>"],
|
|
8
|
+
"background": {
|
|
9
|
+
"service_worker": "background.js"
|
|
10
|
+
},
|
|
11
|
+
"content_scripts": [
|
|
12
|
+
{
|
|
13
|
+
"matches": ["<all_urls>"],
|
|
14
|
+
"js": ["content-script.js"],
|
|
15
|
+
"run_at": "document_start"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
|
6
|
+
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
7
|
+
import { type AnyAgentTool, type OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
8
|
+
|
|
9
|
+
type RrwebReplayConfig = {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
serverUrl?: string;
|
|
12
|
+
publicKey?: string;
|
|
13
|
+
secretKey?: string;
|
|
14
|
+
publicKeyEnvVar?: string;
|
|
15
|
+
secretKeyEnvVar?: string;
|
|
16
|
+
extensionMode: "bundled" | "external-path";
|
|
17
|
+
extensionPath?: string;
|
|
18
|
+
browserProfiles: string[];
|
|
19
|
+
recordingPolicy: "browser-only" | "opt-in-tool";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ReplaySessionState = {
|
|
23
|
+
sessionKey?: string;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
replaySessionId: string;
|
|
26
|
+
replayServerUrl?: string;
|
|
27
|
+
replayUrl?: string;
|
|
28
|
+
currentProfile?: string;
|
|
29
|
+
currentCdpUrl?: string;
|
|
30
|
+
recordingPolicy: RrwebReplayConfig["recordingPolicy"];
|
|
31
|
+
status: "active" | "ended";
|
|
32
|
+
reason?: string;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ReplayStateStore = {
|
|
37
|
+
sessions: Record<string, ReplaySessionState>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const RRWEB_PLUGIN_ID = "rrweb-replay";
|
|
41
|
+
const DEFAULT_RRWEB_API_BASE_URL = "https://api.rrwebcloud.com";
|
|
42
|
+
const LEGACY_BROWSER_RUNTIME_REASON =
|
|
43
|
+
"This OpenClaw host does not export plugin-sdk/browser-runtime. Automatic rrweb extension wiring requires OpenClaw >=2026.3.24. On legacy hosts, install/configure the rrweb browser addon separately or upgrade OpenClaw.";
|
|
44
|
+
|
|
45
|
+
type BrowserRuntimeCompat = {
|
|
46
|
+
clearManagedBrowserReplayContextsForSession: (sessionKey: string) => void;
|
|
47
|
+
registerManagedBrowserExtensions: (params: {
|
|
48
|
+
sourceId: string;
|
|
49
|
+
profiles: string[];
|
|
50
|
+
extensionPaths: string[];
|
|
51
|
+
}) => void;
|
|
52
|
+
setManagedBrowserReplayContext: (params: {
|
|
53
|
+
cdpUrl: string;
|
|
54
|
+
profile: string;
|
|
55
|
+
sessionKey?: string;
|
|
56
|
+
sessionId?: string;
|
|
57
|
+
replaySessionId: string;
|
|
58
|
+
replayServerUrl?: string;
|
|
59
|
+
replayUrl?: string;
|
|
60
|
+
updatedAt: string;
|
|
61
|
+
}) => void;
|
|
62
|
+
unregisterManagedBrowserExtensions: (sourceId: string) => void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const rrwebReplayConfigSchema = {
|
|
66
|
+
parse(value: unknown): RrwebReplayConfig {
|
|
67
|
+
const raw =
|
|
68
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
69
|
+
? (value as Record<string, unknown>)
|
|
70
|
+
: {};
|
|
71
|
+
const browserProfiles = Array.isArray(raw.browserProfiles)
|
|
72
|
+
? raw.browserProfiles
|
|
73
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
74
|
+
.map((entry) => entry.trim())
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
: [];
|
|
77
|
+
const extensionMode = raw.extensionMode === "external-path" ? "external-path" : "bundled";
|
|
78
|
+
const recordingPolicy = raw.recordingPolicy === "opt-in-tool" ? "opt-in-tool" : "browser-only";
|
|
79
|
+
const readOptional = (key: string) => {
|
|
80
|
+
const value = raw[key];
|
|
81
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
enabled: raw.enabled !== false,
|
|
85
|
+
serverUrl: readOptional("serverUrl") ?? DEFAULT_RRWEB_API_BASE_URL,
|
|
86
|
+
publicKey: readOptional("publicKey"),
|
|
87
|
+
secretKey: readOptional("secretKey"),
|
|
88
|
+
publicKeyEnvVar: readOptional("publicKeyEnvVar"),
|
|
89
|
+
secretKeyEnvVar: readOptional("secretKeyEnvVar"),
|
|
90
|
+
extensionMode,
|
|
91
|
+
extensionPath: readOptional("extensionPath"),
|
|
92
|
+
browserProfiles: browserProfiles.length > 0 ? browserProfiles : ["openclaw"],
|
|
93
|
+
recordingPolicy,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
uiHints: {
|
|
97
|
+
serverUrl: {
|
|
98
|
+
label: "Replay Server URL",
|
|
99
|
+
placeholder: DEFAULT_RRWEB_API_BASE_URL,
|
|
100
|
+
advanced: true,
|
|
101
|
+
help: "Optional. Defaults to the rrweb Cloud API endpoint used by the bundled browser replay flow.",
|
|
102
|
+
},
|
|
103
|
+
publicKey: { label: "Public Key", sensitive: true },
|
|
104
|
+
secretKey: {
|
|
105
|
+
label: "Secret Key",
|
|
106
|
+
sensitive: true,
|
|
107
|
+
advanced: true,
|
|
108
|
+
help: "Optional. Reserved for future server-side upload flows and not required for the bundled browser bootstrap.",
|
|
109
|
+
},
|
|
110
|
+
publicKeyEnvVar: { label: "Public Key Env Var", advanced: true },
|
|
111
|
+
secretKeyEnvVar: {
|
|
112
|
+
label: "Secret Key Env Var",
|
|
113
|
+
advanced: true,
|
|
114
|
+
help: "Optional. Only needed when a future rrweb upload path requires a secret key.",
|
|
115
|
+
},
|
|
116
|
+
extensionMode: { label: "Extension Mode" },
|
|
117
|
+
extensionPath: { label: "External Extension Path", advanced: true },
|
|
118
|
+
browserProfiles: { label: "Browser Profiles" },
|
|
119
|
+
recordingPolicy: { label: "Recording Policy" },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function normalizeServerUrl(value: string | undefined): string | undefined {
|
|
124
|
+
const trimmed = value?.trim();
|
|
125
|
+
if (!trimmed) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return trimmed.replace(/\/+$/, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function jsonToolResult(payload: unknown) {
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: "text" as const,
|
|
136
|
+
text: JSON.stringify(payload, null, 2),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
details: payload,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveConfiguredSecret(params: {
|
|
144
|
+
value?: string;
|
|
145
|
+
envVarName?: string;
|
|
146
|
+
}): string | undefined {
|
|
147
|
+
const direct = params.value?.trim();
|
|
148
|
+
if (direct) {
|
|
149
|
+
return direct;
|
|
150
|
+
}
|
|
151
|
+
const envVarName = params.envVarName?.trim();
|
|
152
|
+
if (!envVarName) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const fromEnv = process.env[envVarName];
|
|
156
|
+
return typeof fromEnv === "string" && fromEnv.trim() ? fromEnv.trim() : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveExtensionDirectory(params: {
|
|
160
|
+
config: RrwebReplayConfig;
|
|
161
|
+
api: OpenClawPluginApi;
|
|
162
|
+
}): string | null {
|
|
163
|
+
const rootDir = params.api.rootDir;
|
|
164
|
+
if (!rootDir) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const extensionDir =
|
|
168
|
+
params.config.extensionMode === "external-path"
|
|
169
|
+
? params.config.extensionPath
|
|
170
|
+
: path.join(rootDir, "extension");
|
|
171
|
+
const resolved =
|
|
172
|
+
params.config.extensionMode === "external-path" && extensionDir
|
|
173
|
+
? params.api.resolvePath(extensionDir)
|
|
174
|
+
: (extensionDir ?? "");
|
|
175
|
+
if (!resolved || !fs.existsSync(resolved)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return resolved;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolvePluginStateFile(api: OpenClawPluginApi): string {
|
|
182
|
+
return path.join(
|
|
183
|
+
api.runtime.state.resolveStateDir(),
|
|
184
|
+
"plugins",
|
|
185
|
+
RRWEB_PLUGIN_ID,
|
|
186
|
+
"sessions.json",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function loadReplayState(api: OpenClawPluginApi): Promise<ReplayStateStore> {
|
|
191
|
+
const filePath = resolvePluginStateFile(api);
|
|
192
|
+
const { value } = await readJsonFileWithFallback<ReplayStateStore>(filePath, { sessions: {} });
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function saveReplayState(api: OpenClawPluginApi, state: ReplayStateStore): Promise<void> {
|
|
197
|
+
const filePath = resolvePluginStateFile(api);
|
|
198
|
+
await writeJsonFileAtomically(filePath, state);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildReplaySessionState(params: {
|
|
202
|
+
sessionKey?: string;
|
|
203
|
+
sessionId?: string;
|
|
204
|
+
config: RrwebReplayConfig;
|
|
205
|
+
reason?: string;
|
|
206
|
+
previous?: ReplaySessionState;
|
|
207
|
+
}): ReplaySessionState {
|
|
208
|
+
const updatedAt = new Date().toISOString();
|
|
209
|
+
const replaySessionId =
|
|
210
|
+
params.sessionId?.trim() || params.previous?.replaySessionId || crypto.randomUUID();
|
|
211
|
+
return {
|
|
212
|
+
sessionKey: params.sessionKey,
|
|
213
|
+
sessionId: params.sessionId,
|
|
214
|
+
replaySessionId,
|
|
215
|
+
replayServerUrl: normalizeServerUrl(params.config.serverUrl),
|
|
216
|
+
replayUrl: params.previous?.replayUrl,
|
|
217
|
+
currentProfile: params.previous?.currentProfile,
|
|
218
|
+
currentCdpUrl: params.previous?.currentCdpUrl,
|
|
219
|
+
recordingPolicy: params.config.recordingPolicy,
|
|
220
|
+
status: "active",
|
|
221
|
+
...(params.reason ? { reason: params.reason } : {}),
|
|
222
|
+
updatedAt,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function describeReplayAvailability(params: {
|
|
227
|
+
config: RrwebReplayConfig;
|
|
228
|
+
extensionDir: string | null;
|
|
229
|
+
browserRuntimeAvailable: boolean;
|
|
230
|
+
}): { enabled: boolean; reason?: string } {
|
|
231
|
+
if (!params.config.enabled) {
|
|
232
|
+
return { enabled: false, reason: "Plugin disabled in rrweb-replay config." };
|
|
233
|
+
}
|
|
234
|
+
if (!params.browserRuntimeAvailable) {
|
|
235
|
+
return {
|
|
236
|
+
enabled: false,
|
|
237
|
+
reason: LEGACY_BROWSER_RUNTIME_REASON,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (!params.extensionDir) {
|
|
241
|
+
return {
|
|
242
|
+
enabled: false,
|
|
243
|
+
reason: "Replay extension directory is missing. Check rrweb-replay.extensionMode/path.",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const publicKey = resolveConfiguredSecret({
|
|
247
|
+
value: params.config.publicKey,
|
|
248
|
+
envVarName: params.config.publicKeyEnvVar,
|
|
249
|
+
});
|
|
250
|
+
if (!publicKey) {
|
|
251
|
+
return {
|
|
252
|
+
enabled: false,
|
|
253
|
+
reason:
|
|
254
|
+
"Replay public key is not configured. Set publicKey or publicKeyEnvVar. The API endpoint already defaults to rrweb Cloud, and secretKey is optional for the bundled browser bootstrap.",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return { enabled: true };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function loadBrowserRuntimeCompat(): Promise<BrowserRuntimeCompat | null> {
|
|
261
|
+
try {
|
|
262
|
+
const mod = (await import("openclaw/plugin-sdk/browser-runtime")) as BrowserRuntimeCompat;
|
|
263
|
+
if (
|
|
264
|
+
typeof mod.registerManagedBrowserExtensions === "function" &&
|
|
265
|
+
typeof mod.unregisterManagedBrowserExtensions === "function" &&
|
|
266
|
+
typeof mod.setManagedBrowserReplayContext === "function" &&
|
|
267
|
+
typeof mod.clearManagedBrowserReplayContextsForSession === "function"
|
|
268
|
+
) {
|
|
269
|
+
return mod;
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// Legacy OpenClaw hosts do not expose plugin-sdk/browser-runtime.
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function resolveBrowserProfileForReplay(params: {
|
|
278
|
+
config: OpenClawConfig;
|
|
279
|
+
preferredProfile?: string;
|
|
280
|
+
}): { name: string; cdpUrl?: string; driver?: string } | null {
|
|
281
|
+
const browser = params.config.browser;
|
|
282
|
+
const profiles =
|
|
283
|
+
browser?.profiles && typeof browser.profiles === "object" ? browser.profiles : undefined;
|
|
284
|
+
const profileName =
|
|
285
|
+
params.preferredProfile?.trim() || browser?.defaultProfile?.trim() || "openclaw";
|
|
286
|
+
const rawProfile =
|
|
287
|
+
profiles && typeof profiles[profileName] === "object" && profiles[profileName] != null
|
|
288
|
+
? (profiles[profileName] as Record<string, unknown>)
|
|
289
|
+
: null;
|
|
290
|
+
if (!rawProfile) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const driver = typeof rawProfile.driver === "string" ? rawProfile.driver : undefined;
|
|
294
|
+
if (driver === "existing-session") {
|
|
295
|
+
return { name: profileName, driver };
|
|
296
|
+
}
|
|
297
|
+
const rawCdpUrl =
|
|
298
|
+
typeof rawProfile.cdpUrl === "string" && rawProfile.cdpUrl.trim()
|
|
299
|
+
? rawProfile.cdpUrl.trim()
|
|
300
|
+
: typeof browser?.cdpUrl === "string" && browser.cdpUrl.trim()
|
|
301
|
+
? browser.cdpUrl.trim()
|
|
302
|
+
: undefined;
|
|
303
|
+
if (rawCdpUrl) {
|
|
304
|
+
return { name: profileName, cdpUrl: rawCdpUrl.replace(/\/+$/, ""), driver };
|
|
305
|
+
}
|
|
306
|
+
const cdpPort =
|
|
307
|
+
typeof rawProfile.cdpPort === "number" && Number.isFinite(rawProfile.cdpPort)
|
|
308
|
+
? rawProfile.cdpPort
|
|
309
|
+
: undefined;
|
|
310
|
+
if (cdpPort) {
|
|
311
|
+
return { name: profileName, cdpUrl: `http://127.0.0.1:${cdpPort}`, driver };
|
|
312
|
+
}
|
|
313
|
+
return { name: profileName, driver };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function upsertReplaySession(params: {
|
|
317
|
+
api: OpenClawPluginApi;
|
|
318
|
+
config: RrwebReplayConfig;
|
|
319
|
+
sessionKey?: string;
|
|
320
|
+
sessionId?: string;
|
|
321
|
+
mutate?: (entry: ReplaySessionState) => ReplaySessionState;
|
|
322
|
+
}): Promise<ReplaySessionState> {
|
|
323
|
+
const state = await loadReplayState(params.api);
|
|
324
|
+
const storeKey = params.sessionKey?.trim() || params.sessionId?.trim() || crypto.randomUUID();
|
|
325
|
+
const previous = state.sessions[storeKey];
|
|
326
|
+
const base = buildReplaySessionState({
|
|
327
|
+
sessionKey: params.sessionKey,
|
|
328
|
+
sessionId: params.sessionId,
|
|
329
|
+
config: params.config,
|
|
330
|
+
previous,
|
|
331
|
+
});
|
|
332
|
+
const next = params.mutate ? params.mutate(base) : base;
|
|
333
|
+
state.sessions[storeKey] = next;
|
|
334
|
+
await saveReplayState(params.api, state);
|
|
335
|
+
return next;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function getReplaySessionEntry(params: {
|
|
339
|
+
api: OpenClawPluginApi;
|
|
340
|
+
sessionKey?: string;
|
|
341
|
+
sessionId?: string;
|
|
342
|
+
}): Promise<ReplaySessionState | null> {
|
|
343
|
+
const state = await loadReplayState(params.api);
|
|
344
|
+
const bySessionKey = params.sessionKey?.trim();
|
|
345
|
+
if (bySessionKey && state.sessions[bySessionKey]) {
|
|
346
|
+
return state.sessions[bySessionKey];
|
|
347
|
+
}
|
|
348
|
+
const sessionId = params.sessionId?.trim();
|
|
349
|
+
if (!sessionId) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
for (const entry of Object.values(state.sessions)) {
|
|
353
|
+
if (entry.sessionId === sessionId) {
|
|
354
|
+
return entry;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function markReplaySessionEnded(params: {
|
|
361
|
+
api: OpenClawPluginApi;
|
|
362
|
+
sessionKey?: string;
|
|
363
|
+
sessionId?: string;
|
|
364
|
+
}): Promise<void> {
|
|
365
|
+
const state = await loadReplayState(params.api);
|
|
366
|
+
const storeKey = params.sessionKey?.trim();
|
|
367
|
+
if (storeKey && state.sessions[storeKey]) {
|
|
368
|
+
state.sessions[storeKey] = {
|
|
369
|
+
...state.sessions[storeKey],
|
|
370
|
+
status: "ended",
|
|
371
|
+
updatedAt: new Date().toISOString(),
|
|
372
|
+
};
|
|
373
|
+
await saveReplayState(params.api, state);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!params.sessionId?.trim()) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
for (const [key, entry] of Object.entries(state.sessions)) {
|
|
380
|
+
if (entry.sessionId !== params.sessionId) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
state.sessions[key] = {
|
|
384
|
+
...entry,
|
|
385
|
+
status: "ended",
|
|
386
|
+
updatedAt: new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
await saveReplayState(params.api, state);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function armReplayContext(params: {
|
|
394
|
+
api: OpenClawPluginApi;
|
|
395
|
+
pluginConfig: RrwebReplayConfig;
|
|
396
|
+
browserRuntime: BrowserRuntimeCompat | null;
|
|
397
|
+
sessionKey?: string;
|
|
398
|
+
sessionId?: string;
|
|
399
|
+
preferredProfile?: string;
|
|
400
|
+
}): Promise<ReplaySessionState> {
|
|
401
|
+
const replayEntry =
|
|
402
|
+
(await getReplaySessionEntry({
|
|
403
|
+
api: params.api,
|
|
404
|
+
sessionKey: params.sessionKey,
|
|
405
|
+
sessionId: params.sessionId,
|
|
406
|
+
})) ??
|
|
407
|
+
(await upsertReplaySession({
|
|
408
|
+
api: params.api,
|
|
409
|
+
config: params.pluginConfig,
|
|
410
|
+
sessionKey: params.sessionKey,
|
|
411
|
+
sessionId: params.sessionId,
|
|
412
|
+
}));
|
|
413
|
+
const profile = resolveBrowserProfileForReplay({
|
|
414
|
+
config: params.api.config,
|
|
415
|
+
preferredProfile: params.preferredProfile ?? replayEntry.currentProfile,
|
|
416
|
+
});
|
|
417
|
+
if (!params.browserRuntime || !profile || !profile.cdpUrl) {
|
|
418
|
+
return replayEntry;
|
|
419
|
+
}
|
|
420
|
+
params.browserRuntime.setManagedBrowserReplayContext({
|
|
421
|
+
cdpUrl: profile.cdpUrl,
|
|
422
|
+
profile: profile.name,
|
|
423
|
+
sessionKey: params.sessionKey,
|
|
424
|
+
sessionId: params.sessionId,
|
|
425
|
+
replaySessionId: replayEntry.replaySessionId,
|
|
426
|
+
replayServerUrl: replayEntry.replayServerUrl,
|
|
427
|
+
replayUrl: replayEntry.replayUrl,
|
|
428
|
+
updatedAt: new Date().toISOString(),
|
|
429
|
+
});
|
|
430
|
+
return await upsertReplaySession({
|
|
431
|
+
api: params.api,
|
|
432
|
+
config: params.pluginConfig,
|
|
433
|
+
sessionKey: params.sessionKey,
|
|
434
|
+
sessionId: params.sessionId,
|
|
435
|
+
mutate: (entry) => ({
|
|
436
|
+
...entry,
|
|
437
|
+
replaySessionId: replayEntry.replaySessionId,
|
|
438
|
+
currentProfile: profile.name,
|
|
439
|
+
currentCdpUrl: profile.cdpUrl,
|
|
440
|
+
updatedAt: new Date().toISOString(),
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createReplaySessionInfoTool(params: {
|
|
446
|
+
api: OpenClawPluginApi;
|
|
447
|
+
pluginConfig: RrwebReplayConfig;
|
|
448
|
+
extensionDir: string | null;
|
|
449
|
+
browserRuntimePromise: Promise<BrowserRuntimeCompat | null>;
|
|
450
|
+
context: {
|
|
451
|
+
sessionKey?: string;
|
|
452
|
+
sessionId?: string;
|
|
453
|
+
};
|
|
454
|
+
}): AnyAgentTool {
|
|
455
|
+
return {
|
|
456
|
+
name: "replay_session_info",
|
|
457
|
+
label: "Replay Session Info",
|
|
458
|
+
description:
|
|
459
|
+
"Show the current rrweb replay session id, targeted browser profile, and replay readiness.",
|
|
460
|
+
parameters: Type.Object({
|
|
461
|
+
activate: Type.Optional(
|
|
462
|
+
Type.Boolean({
|
|
463
|
+
description:
|
|
464
|
+
"When true, arm replay context for the current session/profile. Useful with recordingPolicy=opt-in-tool.",
|
|
465
|
+
}),
|
|
466
|
+
),
|
|
467
|
+
profile: Type.Optional(
|
|
468
|
+
Type.String({
|
|
469
|
+
description: "Optional browser profile override when activating replay context.",
|
|
470
|
+
}),
|
|
471
|
+
),
|
|
472
|
+
}),
|
|
473
|
+
execute: async (_id, rawParams) => {
|
|
474
|
+
const browserRuntime = await params.browserRuntimePromise;
|
|
475
|
+
const toolParams =
|
|
476
|
+
rawParams && typeof rawParams === "object" ? (rawParams as Record<string, unknown>) : null;
|
|
477
|
+
const availability = describeReplayAvailability({
|
|
478
|
+
config: params.pluginConfig,
|
|
479
|
+
extensionDir: params.extensionDir,
|
|
480
|
+
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
481
|
+
});
|
|
482
|
+
const activate = toolParams?.activate === true;
|
|
483
|
+
const preferredProfile = toolParams
|
|
484
|
+
? typeof toolParams.profile === "string" && toolParams.profile.trim()
|
|
485
|
+
? toolParams.profile.trim()
|
|
486
|
+
: undefined
|
|
487
|
+
: undefined;
|
|
488
|
+
let entry =
|
|
489
|
+
(await getReplaySessionEntry({
|
|
490
|
+
api: params.api,
|
|
491
|
+
sessionKey: params.context.sessionKey,
|
|
492
|
+
sessionId: params.context.sessionId,
|
|
493
|
+
})) ??
|
|
494
|
+
(await upsertReplaySession({
|
|
495
|
+
api: params.api,
|
|
496
|
+
config: params.pluginConfig,
|
|
497
|
+
sessionKey: params.context.sessionKey,
|
|
498
|
+
sessionId: params.context.sessionId,
|
|
499
|
+
mutate: (next) => ({
|
|
500
|
+
...next,
|
|
501
|
+
...(availability.reason ? { reason: availability.reason } : {}),
|
|
502
|
+
}),
|
|
503
|
+
}));
|
|
504
|
+
|
|
505
|
+
if (activate && availability.enabled) {
|
|
506
|
+
entry = await armReplayContext({
|
|
507
|
+
api: params.api,
|
|
508
|
+
pluginConfig: params.pluginConfig,
|
|
509
|
+
browserRuntime,
|
|
510
|
+
sessionKey: params.context.sessionKey,
|
|
511
|
+
sessionId: params.context.sessionId,
|
|
512
|
+
preferredProfile,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return jsonToolResult({
|
|
517
|
+
ok: true,
|
|
518
|
+
replaySessionId: entry.replaySessionId,
|
|
519
|
+
replayUrl: entry.replayUrl ?? null,
|
|
520
|
+
replayServerUrl: entry.replayServerUrl ?? null,
|
|
521
|
+
browserProfile: entry.currentProfile ?? preferredProfile ?? null,
|
|
522
|
+
cdpUrl: entry.currentCdpUrl ?? null,
|
|
523
|
+
recordingPolicy: entry.recordingPolicy,
|
|
524
|
+
enabled: availability.enabled,
|
|
525
|
+
reason: availability.reason ?? entry.reason ?? null,
|
|
526
|
+
activated: activate && availability.enabled,
|
|
527
|
+
compatibilityMode: browserRuntime ? "modern" : "legacy",
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export default definePluginEntry({
|
|
534
|
+
id: RRWEB_PLUGIN_ID,
|
|
535
|
+
name: "rrweb Replay",
|
|
536
|
+
description: "Portable rrweb replay bootstrap for OpenClaw-managed browsers.",
|
|
537
|
+
configSchema: rrwebReplayConfigSchema,
|
|
538
|
+
register(api: OpenClawPluginApi) {
|
|
539
|
+
const pluginConfig = rrwebReplayConfigSchema.parse(api.pluginConfig);
|
|
540
|
+
const extensionDir = resolveExtensionDirectory({ config: pluginConfig, api });
|
|
541
|
+
const serviceSourceId = `${RRWEB_PLUGIN_ID}:${api.source}`;
|
|
542
|
+
const browserRuntimePromise = loadBrowserRuntimeCompat();
|
|
543
|
+
|
|
544
|
+
api.registerService({
|
|
545
|
+
id: "rrweb-replay-browser-extension",
|
|
546
|
+
async start(ctx) {
|
|
547
|
+
const browserRuntime = await browserRuntimePromise;
|
|
548
|
+
if (!pluginConfig.enabled) {
|
|
549
|
+
ctx.logger.info("[rrweb-replay] disabled; not registering browser extension");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (!browserRuntime) {
|
|
553
|
+
ctx.logger.warn(`[rrweb-replay] ${LEGACY_BROWSER_RUNTIME_REASON}`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (!extensionDir) {
|
|
557
|
+
ctx.logger.warn(
|
|
558
|
+
"[rrweb-replay] extension directory missing; replay browser extension was not registered",
|
|
559
|
+
);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
browserRuntime.registerManagedBrowserExtensions({
|
|
563
|
+
sourceId: serviceSourceId,
|
|
564
|
+
profiles: pluginConfig.browserProfiles,
|
|
565
|
+
extensionPaths: [extensionDir],
|
|
566
|
+
});
|
|
567
|
+
ctx.logger.info(
|
|
568
|
+
`[rrweb-replay] registered extension for profiles: ${pluginConfig.browserProfiles.join(", ")}`,
|
|
569
|
+
);
|
|
570
|
+
},
|
|
571
|
+
async stop() {
|
|
572
|
+
const browserRuntime = await browserRuntimePromise;
|
|
573
|
+
browserRuntime?.unregisterManagedBrowserExtensions(serviceSourceId);
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
api.on("session_start", async (event, ctx) => {
|
|
578
|
+
const browserRuntime = await browserRuntimePromise;
|
|
579
|
+
const availability = describeReplayAvailability({
|
|
580
|
+
config: pluginConfig,
|
|
581
|
+
extensionDir,
|
|
582
|
+
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
583
|
+
});
|
|
584
|
+
await upsertReplaySession({
|
|
585
|
+
api,
|
|
586
|
+
config: pluginConfig,
|
|
587
|
+
sessionKey: ctx.sessionKey,
|
|
588
|
+
sessionId: event.sessionId,
|
|
589
|
+
mutate: (entry) => ({
|
|
590
|
+
...entry,
|
|
591
|
+
...(availability.reason ? { reason: availability.reason } : {}),
|
|
592
|
+
}),
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
api.on("before_reset", async (_event, ctx) => {
|
|
597
|
+
const browserRuntime = await browserRuntimePromise;
|
|
598
|
+
if (ctx.sessionKey && browserRuntime) {
|
|
599
|
+
browserRuntime.clearManagedBrowserReplayContextsForSession(ctx.sessionKey);
|
|
600
|
+
}
|
|
601
|
+
await markReplaySessionEnded({
|
|
602
|
+
api,
|
|
603
|
+
sessionKey: ctx.sessionKey,
|
|
604
|
+
sessionId: ctx.sessionId,
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
api.on("session_end", async (_event, ctx) => {
|
|
609
|
+
const browserRuntime = await browserRuntimePromise;
|
|
610
|
+
if (ctx.sessionKey && browserRuntime) {
|
|
611
|
+
browserRuntime.clearManagedBrowserReplayContextsForSession(ctx.sessionKey);
|
|
612
|
+
}
|
|
613
|
+
await markReplaySessionEnded({
|
|
614
|
+
api,
|
|
615
|
+
sessionKey: ctx.sessionKey,
|
|
616
|
+
sessionId: ctx.sessionId,
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
621
|
+
const browserRuntime = await browserRuntimePromise;
|
|
622
|
+
const availability = describeReplayAvailability({
|
|
623
|
+
config: pluginConfig,
|
|
624
|
+
extensionDir,
|
|
625
|
+
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
626
|
+
});
|
|
627
|
+
if (event.toolName !== "browser" || pluginConfig.recordingPolicy !== "browser-only") {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (!availability.enabled) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const rawProfile =
|
|
634
|
+
typeof event.params.profile === "string" && event.params.profile.trim()
|
|
635
|
+
? event.params.profile.trim()
|
|
636
|
+
: undefined;
|
|
637
|
+
await armReplayContext({
|
|
638
|
+
api,
|
|
639
|
+
pluginConfig,
|
|
640
|
+
browserRuntime,
|
|
641
|
+
sessionKey: ctx.sessionKey,
|
|
642
|
+
sessionId: ctx.sessionId,
|
|
643
|
+
preferredProfile: rawProfile,
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
api.registerTool(
|
|
648
|
+
(context) =>
|
|
649
|
+
createReplaySessionInfoTool({
|
|
650
|
+
api,
|
|
651
|
+
pluginConfig,
|
|
652
|
+
extensionDir,
|
|
653
|
+
browserRuntimePromise,
|
|
654
|
+
context: {
|
|
655
|
+
sessionKey: context.sessionKey,
|
|
656
|
+
sessionId: context.sessionId,
|
|
657
|
+
},
|
|
658
|
+
}),
|
|
659
|
+
{ name: "replay_session_info", optional: true },
|
|
660
|
+
);
|
|
661
|
+
},
|
|
662
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "rrweb-replay",
|
|
3
|
+
"name": "rrweb Replay",
|
|
4
|
+
"description": "Portable rrweb replay bootstrap plugin for OpenClaw-managed browsers.",
|
|
5
|
+
"skills": ["./skills"],
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"enabled": { "type": "boolean" },
|
|
11
|
+
"serverUrl": { "type": "string" },
|
|
12
|
+
"publicKey": { "type": "string" },
|
|
13
|
+
"secretKey": { "type": "string" },
|
|
14
|
+
"publicKeyEnvVar": { "type": "string" },
|
|
15
|
+
"secretKeyEnvVar": { "type": "string" },
|
|
16
|
+
"extensionMode": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"enum": ["bundled", "external-path"]
|
|
19
|
+
},
|
|
20
|
+
"extensionPath": { "type": "string" },
|
|
21
|
+
"browserProfiles": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": { "type": "string" }
|
|
24
|
+
},
|
|
25
|
+
"recordingPolicy": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["browser-only", "opt-in-tool"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"uiHints": {
|
|
32
|
+
"serverUrl": {
|
|
33
|
+
"label": "Replay Server URL",
|
|
34
|
+
"help": "Optional. Defaults to the rrweb Cloud API endpoint used by the bundled browser replay flow.",
|
|
35
|
+
"placeholder": "https://api.rrwebcloud.com",
|
|
36
|
+
"advanced": true
|
|
37
|
+
},
|
|
38
|
+
"publicKey": {
|
|
39
|
+
"label": "Public Key",
|
|
40
|
+
"sensitive": true
|
|
41
|
+
},
|
|
42
|
+
"secretKey": {
|
|
43
|
+
"label": "Secret Key",
|
|
44
|
+
"sensitive": true,
|
|
45
|
+
"advanced": true,
|
|
46
|
+
"help": "Optional. Reserved for future server-side upload flows and not required for the bundled browser bootstrap."
|
|
47
|
+
},
|
|
48
|
+
"publicKeyEnvVar": {
|
|
49
|
+
"label": "Public Key Env Var",
|
|
50
|
+
"advanced": true
|
|
51
|
+
},
|
|
52
|
+
"secretKeyEnvVar": {
|
|
53
|
+
"label": "Secret Key Env Var",
|
|
54
|
+
"advanced": true,
|
|
55
|
+
"help": "Optional. Only needed when a future rrweb upload path requires a secret key."
|
|
56
|
+
},
|
|
57
|
+
"extensionMode": {
|
|
58
|
+
"label": "Extension Mode"
|
|
59
|
+
},
|
|
60
|
+
"extensionPath": {
|
|
61
|
+
"label": "External Extension Path",
|
|
62
|
+
"advanced": true
|
|
63
|
+
},
|
|
64
|
+
"browserProfiles": {
|
|
65
|
+
"label": "Browser Profiles"
|
|
66
|
+
},
|
|
67
|
+
"recordingPolicy": {
|
|
68
|
+
"label": "Recording Policy"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rrwebcloud/openclaw-session-recording",
|
|
3
|
+
"version": "2026.3.28-1",
|
|
4
|
+
"description": "OpenClaw rrweb replay plugin",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
".codex-plugin/",
|
|
9
|
+
"README.md",
|
|
10
|
+
"extension/",
|
|
11
|
+
"index.ts",
|
|
12
|
+
"openclaw.plugin.json",
|
|
13
|
+
"skills/"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@sinclair/typebox": "0.34.48"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"openclaw": "workspace:*"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"openclaw": ">=2026.3.13"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"openclaw": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"openclaw": {
|
|
33
|
+
"extensions": ["./index.ts"],
|
|
34
|
+
"install": {
|
|
35
|
+
"npmSpec": "@rrwebcloud/openclaw-session-recording",
|
|
36
|
+
"localPath": "extensions/rrweb-replay",
|
|
37
|
+
"defaultChoice": "npm",
|
|
38
|
+
"minHostVersion": ">=2026.3.13"
|
|
39
|
+
},
|
|
40
|
+
"release": {
|
|
41
|
+
"publishToNpm": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# rrweb Replay
|
|
2
|
+
|
|
3
|
+
Use this skill when you need session replay metadata for OpenClaw's managed browser.
|
|
4
|
+
|
|
5
|
+
What it does:
|
|
6
|
+
|
|
7
|
+
- Explains whether the `rrweb-replay` plugin is configured and active
|
|
8
|
+
- Shows the current replay session id via `replay_session_info`
|
|
9
|
+
- Helps activate replay context when `recordingPolicy` is `opt-in-tool`
|
|
10
|
+
- Points you to browser profile and extension troubleshooting
|
|
11
|
+
|
|
12
|
+
Operational notes:
|
|
13
|
+
|
|
14
|
+
- Runtime setup is handled by the native `rrweb-replay` plugin, not by this skill
|
|
15
|
+
- The core browser runtime can also expose replay through `browser.replay.*` when the rrwebcloud extension artifact is wired directly into managed browser startup
|
|
16
|
+
- The plugin targets OpenClaw-managed Chromium profiles such as `openclaw`
|
|
17
|
+
- The streamlined OpenClaw path only needs a public key; the bundled browser bootstrap defaults to the rrweb Cloud API endpoint and does not require a secret key
|
|
18
|
+
- Minimal config:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
plugins:
|
|
22
|
+
entries:
|
|
23
|
+
rrweb-replay:
|
|
24
|
+
enabled: true
|
|
25
|
+
publicKey: pk_live_your_public_key
|
|
26
|
+
extensionMode: bundled
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- If replay reports disabled, check:
|
|
30
|
+
- `browser.replay.enabled`
|
|
31
|
+
- `browser.replay.extensionPath`
|
|
32
|
+
- `plugins.entries.rrweb-replay`
|
|
33
|
+
- `rrweb-replay.extensionMode` / `rrweb-replay.extensionPath`
|
|
34
|
+
- `rrweb-replay.publicKey` or `rrweb-replay.publicKeyEnvVar`
|
|
35
|
+
- `browser.profiles.<name>.extensions` only if you are bypassing the plugin runtime seam
|
|
36
|
+
|
|
37
|
+
Useful command/tool flow:
|
|
38
|
+
|
|
39
|
+
- Run `replay_session_info` to inspect the current replay session
|
|
40
|
+
- Run `replay_session_info` with `activate=true` when replay is opt-in
|
|
41
|
+
- Use the reported `browserProfile` to verify which managed profile is being recorded
|
|
42
|
+
|
|
43
|
+
Troubleshooting:
|
|
44
|
+
|
|
45
|
+
- Missing extension directory: the bundled bootstrap extension was not staged; verify plugin install layout
|
|
46
|
+
- No replay session id: start a new OpenClaw session or use the browser tool so the plugin can bind session metadata
|
|
47
|
+
- Wrong profile: pass `profile` to `replay_session_info activate=true` or use the browser tool with an explicit `profile`
|
|
48
|
+
- Advanced setup: only override `rrweb-replay.serverUrl` if you are targeting a non-default replay API endpoint
|