repoburg 1.3.227 → 1.3.229
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/agent-pane/frontend/dist/index.html +2 -0
- package/backend/dist/src/application-state/application-state.service.js +2 -2
- package/backend/dist/src/seeding/data/sub-agents/general-purpose.d.ts +8 -0
- package/backend/dist/src/seeding/data/sub-agents/general-purpose.js +28 -0
- package/backend/dist/src/seeding/data/sub-agents/general-purpose.js.map +1 -0
- package/backend/dist/src/seeding/data/system-prompts/default_native_tool_agent.d.ts +1 -1
- package/backend/dist/src/seeding/data/system-prompts/default_native_tool_agent.js +1 -0
- package/backend/dist/src/seeding/data/system-prompts/default_native_tool_agent.js.map +1 -1
- package/backend/dist/src/seeding/data/system-prompts/general-purpose-agent.d.ts +4 -0
- package/backend/dist/src/seeding/data/system-prompts/general-purpose-agent.js +71 -0
- package/backend/dist/src/seeding/data/system-prompts/general-purpose-agent.js.map +1 -0
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/daemon/dist/index.js +7 -0
- package/daemon/dist/recorder/collector.js +62 -0
- package/daemon/dist/recorder/recorder.spec.js +86 -0
- package/daemon/dist/recorder/retention.js +40 -0
- package/daemon/dist/recorder/router.js +68 -0
- package/package.json +4 -3
- package/recorder/dist/recorder.js +1 -0
package/daemon/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const agentPane_1 = require("./agentPane");
|
|
|
18
18
|
const routes_1 = require("./remote/routes");
|
|
19
19
|
const checkRemoteAuth_1 = require("./middleware/checkRemoteAuth");
|
|
20
20
|
const staticServing_1 = require("./staticServing");
|
|
21
|
+
const router_1 = require("./recorder/router");
|
|
21
22
|
const PORT = 9998;
|
|
22
23
|
async function main() {
|
|
23
24
|
await serviceManager_1.serviceManager.connect();
|
|
@@ -53,6 +54,12 @@ async function main() {
|
|
|
53
54
|
app.use('/services', services_1.default);
|
|
54
55
|
app.use('/auth', (0, auth_1.default)(wss));
|
|
55
56
|
app.use('/registry', registry_1.default);
|
|
57
|
+
// Session event recorder: POST /recorder/events (collector) + static bundle
|
|
58
|
+
// for the pane's one-line script include. Must precede the pane SPA router.
|
|
59
|
+
// distPath is resolved from THIS file's dir (daemon/src in dev, daemon/dist
|
|
60
|
+
// in prod): ../../recorder/dist — router.ts sits one level deeper, so its
|
|
61
|
+
// own __dirname would resolve one directory short.
|
|
62
|
+
app.use('/recorder', (0, router_1.createRecorderRouter)({ distPath: (0, router_1.resolveRecorderDist)(__dirname) }));
|
|
56
63
|
// MCP Gateway — single MCP entry point managing all instances (streamableHttp).
|
|
57
64
|
app.post('/mcp', (req, res) => mcp_gateway_1.mcpGateway.handleExpressRequest(req, res));
|
|
58
65
|
app.get('/mcp', (req, res) => mcp_gateway_1.mcpGateway.handleExpressRequest(req, res));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EVENT_TYPES = void 0;
|
|
7
|
+
exports.validateSid = validateSid;
|
|
8
|
+
exports.validateEvents = validateEvents;
|
|
9
|
+
exports.appendSessionEvents = appendSessionEvents;
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
/**
|
|
13
|
+
* Collector validation + append logic for the recorder event stream.
|
|
14
|
+
* One JSONL file per session (sid); the file name is derived from the
|
|
15
|
+
* sid, so it must be strictly filename-safe. Lines are appended in
|
|
16
|
+
* arrival order — the recorder guarantees time-ordered events.
|
|
17
|
+
*/
|
|
18
|
+
const SID_RE = /^[a-zA-Z0-9_-]{4,64}$/;
|
|
19
|
+
exports.EVENT_TYPES = new Set([
|
|
20
|
+
'session_start',
|
|
21
|
+
'session_end',
|
|
22
|
+
'route_change',
|
|
23
|
+
'click',
|
|
24
|
+
'scroll',
|
|
25
|
+
'dom_update',
|
|
26
|
+
'request_start',
|
|
27
|
+
'request_end',
|
|
28
|
+
'request_fail',
|
|
29
|
+
'error',
|
|
30
|
+
'long_task',
|
|
31
|
+
'lcp',
|
|
32
|
+
'inp',
|
|
33
|
+
'cls',
|
|
34
|
+
]);
|
|
35
|
+
/** Validate and return the session id, or throw a descriptive error. */
|
|
36
|
+
function validateSid(sid) {
|
|
37
|
+
if (typeof sid !== 'string' || !SID_RE.test(sid)) {
|
|
38
|
+
throw new Error('invalid session id');
|
|
39
|
+
}
|
|
40
|
+
return sid;
|
|
41
|
+
}
|
|
42
|
+
/** Validate a batch body: an array of flat event objects with known types. */
|
|
43
|
+
function validateEvents(events) {
|
|
44
|
+
if (!Array.isArray(events))
|
|
45
|
+
throw new Error('events must be an array');
|
|
46
|
+
for (const ev of events) {
|
|
47
|
+
if (typeof ev !== 'object' || ev === null)
|
|
48
|
+
throw new Error('event must be an object');
|
|
49
|
+
const type = ev.type;
|
|
50
|
+
if (typeof type !== 'string' || !exports.EVENT_TYPES.has(type)) {
|
|
51
|
+
throw new Error(`unknown event type: ${String(type)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return events;
|
|
55
|
+
}
|
|
56
|
+
/** Append a validated batch to <sessionsDir>/<sid>.jsonl. Returns the file path. */
|
|
57
|
+
function appendSessionEvents(sessionsDir, sid, events) {
|
|
58
|
+
const file = path_1.default.join(sessionsDir, `${sid}.jsonl`);
|
|
59
|
+
const lines = events.map((ev) => JSON.stringify(ev)).join('\n') + '\n';
|
|
60
|
+
fs_extra_1.default.appendFileSync(file, lines, 'utf8');
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const collector_1 = require("./collector");
|
|
10
|
+
const retention_1 = require("./retention");
|
|
11
|
+
describe('collector', () => {
|
|
12
|
+
let dir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
dir = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'recorder-test-'));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs_extra_1.default.removeSync(dir);
|
|
18
|
+
});
|
|
19
|
+
it('accepts a filename-safe session id', () => {
|
|
20
|
+
expect((0, collector_1.validateSid)('a91f0c3d44e2')).toBe('a91f0c3d44e2');
|
|
21
|
+
});
|
|
22
|
+
it('rejects path-traversal session ids', () => {
|
|
23
|
+
expect(() => (0, collector_1.validateSid)('../etc')).toThrow();
|
|
24
|
+
expect(() => (0, collector_1.validateSid)('')).toThrow();
|
|
25
|
+
expect(() => (0, collector_1.validateSid)(42)).toThrow();
|
|
26
|
+
expect(() => (0, collector_1.validateSid)('abc')).toThrow(); // too short
|
|
27
|
+
});
|
|
28
|
+
it('accepts valid event batches and rejects malformed ones', () => {
|
|
29
|
+
expect((0, collector_1.validateEvents)([{ type: 'click', target: '#x' }])).toHaveLength(1);
|
|
30
|
+
expect(() => (0, collector_1.validateEvents)('nope')).toThrow();
|
|
31
|
+
expect(() => (0, collector_1.validateEvents)([{ type: 'bogus' }])).toThrow();
|
|
32
|
+
expect(() => (0, collector_1.validateEvents)([null])).toThrow();
|
|
33
|
+
});
|
|
34
|
+
it('appends events as JSONL lines in order', () => {
|
|
35
|
+
const events = [
|
|
36
|
+
{ ts: 1, t: 0, sid: 'sess1234', type: 'session_start', route: '/' },
|
|
37
|
+
{ ts: 2, t: 1, sid: 'sess1234', type: 'click', target: '#send' },
|
|
38
|
+
];
|
|
39
|
+
const file = (0, collector_1.appendSessionEvents)(dir, 'sess1234', events);
|
|
40
|
+
expect(file).toBe(path_1.default.join(dir, 'sess1234.jsonl'));
|
|
41
|
+
const lines = fs_extra_1.default.readFileSync(file, 'utf8').trim().split('\n');
|
|
42
|
+
expect(lines).toHaveLength(2);
|
|
43
|
+
expect(JSON.parse(lines[0]).type).toBe('session_start');
|
|
44
|
+
expect(JSON.parse(lines[1]).target).toBe('#send');
|
|
45
|
+
// A second batch appends to the same file.
|
|
46
|
+
(0, collector_1.appendSessionEvents)(dir, 'sess1234', [{ ts: 3, t: 2, sid: 'sess1234', type: 'error', message: 'x' }]);
|
|
47
|
+
expect(fs_extra_1.default.readFileSync(file, 'utf8').trim().split('\n')).toHaveLength(3);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('retention (ring buffer)', () => {
|
|
51
|
+
let dir;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
dir = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'recorder-ret-'));
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
fs_extra_1.default.removeSync(dir);
|
|
57
|
+
});
|
|
58
|
+
const writeSession = (name, sizeBytes, mtimeMsAgo) => {
|
|
59
|
+
const file = path_1.default.join(dir, `${name}.jsonl`);
|
|
60
|
+
fs_extra_1.default.writeFileSync(file, 'x'.repeat(sizeBytes));
|
|
61
|
+
const past = new Date(Date.now() - mtimeMsAgo);
|
|
62
|
+
fs_extra_1.default.utimesSync(file, past, past);
|
|
63
|
+
};
|
|
64
|
+
it('deletes nothing when under the quota', () => {
|
|
65
|
+
writeSession('aaa11111', 100, 10000);
|
|
66
|
+
const deleted = (0, retention_1.enforceQuota)(dir, 1000);
|
|
67
|
+
expect(deleted).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
it('deletes oldest sessions first until under the quota', () => {
|
|
70
|
+
writeSession('old111111', 500, 30000);
|
|
71
|
+
writeSession('mid111111', 500, 20000);
|
|
72
|
+
writeSession('new111111', 500, 10000);
|
|
73
|
+
// Quota 600: 1500 -> delete old (1000) -> delete mid (500 <= 600) -> stop.
|
|
74
|
+
const deleted = (0, retention_1.enforceQuota)(dir, 600);
|
|
75
|
+
expect(deleted).toEqual([path_1.default.join(dir, 'old111111.jsonl'), path_1.default.join(dir, 'mid111111.jsonl')]);
|
|
76
|
+
expect(fs_extra_1.default.existsSync(path_1.default.join(dir, 'new111111.jsonl'))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it('ignores non-jsonl files and a missing directory', () => {
|
|
79
|
+
fs_extra_1.default.writeFileSync(path_1.default.join(dir, 'notes.txt'), 'keep me');
|
|
80
|
+
writeSession('sess2222', 100, 1000);
|
|
81
|
+
const deleted = (0, retention_1.enforceQuota)(dir, 10);
|
|
82
|
+
expect(deleted).toHaveLength(1);
|
|
83
|
+
expect(fs_extra_1.default.existsSync(path_1.default.join(dir, 'notes.txt'))).toBe(true);
|
|
84
|
+
expect((0, retention_1.enforceQuota)(path_1.default.join(dir, 'missing'), 10)).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.enforceQuota = enforceQuota;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Ring-buffer retention at file granularity: when the total size of the
|
|
11
|
+
* session files exceeds the quota, the OLDEST session files are deleted
|
|
12
|
+
* until the directory is under the cap. The agent only sees sessions
|
|
13
|
+
* that still exist — deleted files are simply absent, never an error.
|
|
14
|
+
*/
|
|
15
|
+
function enforceQuota(sessionsDir, maxBytes) {
|
|
16
|
+
const deleted = [];
|
|
17
|
+
if (!fs_extra_1.default.existsSync(sessionsDir) || maxBytes <= 0)
|
|
18
|
+
return deleted;
|
|
19
|
+
const entries = fs_extra_1.default
|
|
20
|
+
.readdirSync(sessionsDir)
|
|
21
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
22
|
+
.map((f) => {
|
|
23
|
+
const file = path_1.default.join(sessionsDir, f);
|
|
24
|
+
const stat = fs_extra_1.default.statSync(file);
|
|
25
|
+
return { file, size: stat.size, mtime: stat.mtimeMs };
|
|
26
|
+
});
|
|
27
|
+
let total = entries.reduce((sum, e) => sum + e.size, 0);
|
|
28
|
+
if (total <= maxBytes)
|
|
29
|
+
return deleted;
|
|
30
|
+
// Oldest first (mtime ascending) — classic ring-buffer eviction order.
|
|
31
|
+
entries.sort((a, b) => a.mtime - b.mtime);
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (total <= maxBytes)
|
|
34
|
+
break;
|
|
35
|
+
fs_extra_1.default.unlinkSync(entry.file);
|
|
36
|
+
total -= entry.size;
|
|
37
|
+
deleted.push(entry.file);
|
|
38
|
+
}
|
|
39
|
+
return deleted;
|
|
40
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.recorderSessionsDir = recorderSessionsDir;
|
|
7
|
+
exports.resolveRecorderDist = resolveRecorderDist;
|
|
8
|
+
exports.createRecorderRouter = createRecorderRouter;
|
|
9
|
+
const express_1 = __importDefault(require("express"));
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const collector_1 = require("./collector");
|
|
14
|
+
const retention_1 = require("./retention");
|
|
15
|
+
/**
|
|
16
|
+
* Recorder routes, mounted at /recorder on the daemon's existing port
|
|
17
|
+
* (no separate listener, no CORS — the pane is same-origin):
|
|
18
|
+
* POST /recorder/events batched event upload from the browser bundle
|
|
19
|
+
* GET /recorder/recorder.js the static IIFE bundle
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_QUOTA_BYTES = 100 * 1024 * 1024;
|
|
22
|
+
/** Directory holding the JSONL session files. */
|
|
23
|
+
function recorderSessionsDir(home = os_1.default.homedir()) {
|
|
24
|
+
return path_1.default.join(home, '.repoburg', 'recorder', 'sessions');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the recorder bundle ABSOLUTELY (pm2 God-daemon-cwd bug class:
|
|
28
|
+
* never resolve runtime paths against the process cwd).
|
|
29
|
+
* Works for dev (daemon/src or daemon/dist) and the published layout
|
|
30
|
+
* (repoburg/daemon/dist -> repoburg/recorder/dist).
|
|
31
|
+
*/
|
|
32
|
+
function resolveRecorderDist(fromDir) {
|
|
33
|
+
return path_1.default.resolve(fromDir, '../../recorder/dist');
|
|
34
|
+
}
|
|
35
|
+
function createRecorderRouter(opts = {}) {
|
|
36
|
+
const sessionsDir = opts.sessionsDir ?? recorderSessionsDir();
|
|
37
|
+
const maxBytes = opts.maxBytes ??
|
|
38
|
+
(process.env.REPOBURG_RECORDER_MAX_BYTES
|
|
39
|
+
? parseInt(process.env.REPOBURG_RECORDER_MAX_BYTES, 10)
|
|
40
|
+
: DEFAULT_QUOTA_BYTES);
|
|
41
|
+
const router = express_1.default.Router();
|
|
42
|
+
// Batched upload: { sid, events: [...] } -> append to the session file.
|
|
43
|
+
router.post('/events', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const sid = (0, collector_1.validateSid)(req.body?.sid);
|
|
46
|
+
const events = (0, collector_1.validateEvents)(req.body?.events);
|
|
47
|
+
fs_extra_1.default.ensureDirSync(sessionsDir);
|
|
48
|
+
(0, collector_1.appendSessionEvents)(sessionsDir, sid, events);
|
|
49
|
+
(0, retention_1.enforceQuota)(sessionsDir, maxBytes);
|
|
50
|
+
res.status(204).end();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
res.status(400).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Static bundle for the pane's one-line <script> include.
|
|
57
|
+
// Default path is relative to THIS module (daemon/[src|dist]/recorder) —
|
|
58
|
+
// three levels up to the repo/published root. index.ts passes its own
|
|
59
|
+
// resolution (two levels from daemon/[src|dist]) explicitly.
|
|
60
|
+
const distPath = opts.distPath ?? path_1.default.resolve(__dirname, '../../../recorder/dist');
|
|
61
|
+
if (fs_extra_1.default.existsSync(distPath)) {
|
|
62
|
+
router.use(express_1.default.static(distPath));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.warn(`[recorder] bundle not found at ${distPath} — script not served`);
|
|
66
|
+
}
|
|
67
|
+
return router;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repoburg",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.229",
|
|
4
4
|
"description": "A local AI-powered software developer assistant that runs on your own machine.",
|
|
5
5
|
"author": "Celal Ertug",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"backend",
|
|
13
13
|
"website2",
|
|
14
14
|
"daemon",
|
|
15
|
-
"visual-editor-proxy"
|
|
15
|
+
"visual-editor-proxy",
|
|
16
|
+
"recorder"
|
|
16
17
|
],
|
|
17
18
|
"bin": {
|
|
18
19
|
"repoburg": "./platform-cli.js",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"dev:agent-pane": "concurrently -n backend,frontend -c blue,green \"cd agent-pane/backend && npm run dev\" \"cd agent-pane/frontend && npm run dev\"",
|
|
38
39
|
"jaeger:start": "docker run -d --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest",
|
|
39
40
|
"jaeger:stop": "docker stop jaeger",
|
|
40
|
-
"lo:build": "npm run build -w backend && npm run build -w website2 && (cd agent-pane/backend && npm run build) && (cd agent-pane/frontend && npm run build) && npm run build -w daemon && cd frontend && npm run build",
|
|
41
|
+
"lo:build": "npm run build -w backend && npm run build -w website2 && (cd agent-pane/backend && npm run build) && (cd agent-pane/frontend && npm run build) && npm run build -w recorder && npm run build -w daemon && cd frontend && npm run build",
|
|
41
42
|
"lo:publish": "npm publish"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{function l(t,r){return(e,o={})=>{let i={ts:Date.now(),t:Date.now()-t.t0,sid:t.sid,type:e,...o};try{r.push(i)}catch{}}}function S(t){if(!(t instanceof Element))return"";if(t.id)return`#${t.id}`;let r=t.tagName.toLowerCase(),e=t.classList.length?"."+Array.from(t.classList).slice(0,2).join("."):"";return r+e}function f(t){let r=e=>{t("click",{target:S(e.target)})};return document.addEventListener("click",r,!0),()=>document.removeEventListener("click",r,!0)}function m(t,r=500){let e=0,o=null,i=new MutationObserver(n=>{e+=n.length,o===null&&(o=setTimeout(()=>{o=null;let c=e;e=0,c>0&&t("dom_update",{count:c})},r))});try{i.observe(document.documentElement,{childList:!0,subtree:!0})}catch{}return()=>{o!==null&&clearTimeout(o),i.disconnect()}}function h(t){let r=o=>{t("error",{message:o.message})},e=o=>{t("error",{message:String(o.reason?.message??o.reason)})};return window.addEventListener("error",r),window.addEventListener("unhandledrejection",e),()=>{window.removeEventListener("error",r),window.removeEventListener("unhandledrejection",e)}}var T=0,v=()=>`r${++T}`,y=t=>t.includes("/recorder/events");function I(t){return typeof t=="string"?t:t instanceof URL?t.href:t.url}function E(t){let r=[],e=window.fetch.bind(window);window.fetch=(n,c)=>{let a=I(n);if(y(a))return e(n,c);let s=(c?.method??"GET").toUpperCase(),u=v(),d=Date.now();return t("request_start",{reqId:u,method:s,url:a}),e(n,c).then(p=>(t("request_end",{reqId:u,method:s,url:a,status:p.status,dur:Date.now()-d}),p),p=>{throw t("request_fail",{reqId:u,method:s,url:a,message:String(p?.message??p),dur:Date.now()-d}),p})},r.push(()=>{window.fetch=e});let o=XMLHttpRequest.prototype.open,i=XMLHttpRequest.prototype.send;return XMLHttpRequest.prototype.open=function(n,c,...a){return this.__recMethod=String(n).toUpperCase(),this.__recUrl=String(c),o.apply(this,[n,c,...a])},XMLHttpRequest.prototype.send=function(...n){if(y(this.__recUrl??""))return i.apply(this,n);let c=v(),a=this.__recMethod??"GET",s=this.__recUrl??"",u=Date.now();return t("request_start",{reqId:c,method:a,url:s}),this.addEventListener("loadend",()=>{let d=Date.now()-u;this.status===0||this.status>=400?t("request_fail",{reqId:c,method:a,url:s,status:this.status,dur:d}):t("request_end",{reqId:c,method:a,url:s,status:this.status,dur:d})}),i.apply(this,n)},r.push(()=>{XMLHttpRequest.prototype.open=o,XMLHttpRequest.prototype.send=i}),()=>r.forEach(n=>n())}function g(t){let r=[],e=(i,n,c)=>{try{let a=new PerformanceObserver(s=>{for(let u of s.getEntries())n(u)});a.observe({type:i,buffered:!0,...c}),r.push(a)}catch{}};e("longtask",i=>t("long_task",{dur:Math.round(i.duration)})),e("largest-contentful-paint",i=>t("lcp",{value:Math.round(i.startTime)}));let o=new Set;return e("event",i=>{let n=i;n.duration<50||!n.interactionId||o.has(n.interactionId)||(o.size>=1e3&&o.clear(),o.add(n.interactionId),t("inp",{value:Math.round(n.duration)}))},{durationThreshold:16}),e("layout-shift",i=>{let n=i;n.hadRecentInput||t("cls",{value:Math.round(n.value*1e3)/1e3})}),()=>r.forEach(i=>{try{i.disconnect()}catch{}})}function w(t){let r=()=>t("route_change",{route:location.pathname}),e=history.pushState.bind(history),o=history.replaceState.bind(history),i=(...a)=>{let s=e(...a);return r(),s},n=(...a)=>{let s=o(...a);return r(),s};history.pushState=i,history.replaceState=n;let c=()=>r();return window.addEventListener("popstate",c),()=>{history.pushState=e,history.replaceState=o,window.removeEventListener("popstate",c)}}function R(t,r=1e3){let e=0,o=null,i=()=>{e=Date.now(),t("scroll",{y:Math.round(window.scrollY)})},n=()=>{if(Date.now()-e<r){o===null&&(o=setTimeout(()=>{o=null,i()},r));return}i()};return window.addEventListener("scroll",n,{passive:!0}),()=>{o!==null&&clearTimeout(o),window.removeEventListener("scroll",n)}}function q(){try{return crypto.randomUUID().replace(/-/g,"").slice(0,12)}catch{return Array.from({length:12},()=>Math.floor(Math.random()*36).toString(36)).join("")}}function b(){return{sid:q(),t0:Date.now()}}var _="/recorder/events";function L(t,r={}){let e=r.fetch??((...s)=>fetch(...s)),o=r.sendBeacon??((s,u)=>{try{return navigator.sendBeacon(s,u)}catch{return!1}}),i=r.onUnload??(s=>{document.addEventListener("pagehide",s),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&s()})}),n=[],c=(s,u)=>{if(s.length===0)return;let d=JSON.stringify({sid:t,events:s});try{if(u&&o(_,d))return;e(_,{method:"POST",headers:{"content-type":"application/json"},body:d,keepalive:!0}).catch(()=>{})}catch{}},a={push(s){n.push(s),n.length>=100&&a.flush()},flush(s=!1){let u=n;n=[],c(u,s)}};return(r.setInterval??((s,u)=>setInterval(s,u)))(()=>a.flush(),2e3),i(()=>a.flush(!0)),a}(function(){let r=window;if(!r.__repoburgRecorder){r.__repoburgRecorder=!0;try{M()}catch{}}})();function M(){let t=b(),r=L(t.sid),e=l(t,r);e("session_start",{route:location.pathname}),w(e),f(e),R(e),m(e),E(e),h(e),g(e),document.addEventListener("pagehide",()=>{e("session_end"),r.flush(!0)})}})();
|