everything-dev 0.1.3 → 0.1.4
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/package.json +11 -7
- package/src/cli.ts +109 -1
- package/src/components/monitor-view.tsx +471 -0
- package/src/contract.ts +94 -0
- package/src/lib/orchestrator.ts +13 -2
- package/src/lib/resource-monitor/assertions.ts +234 -0
- package/src/lib/resource-monitor/command.ts +283 -0
- package/src/lib/resource-monitor/diff.ts +143 -0
- package/src/lib/resource-monitor/errors.ts +127 -0
- package/src/lib/resource-monitor/index.ts +305 -0
- package/src/lib/resource-monitor/platform/darwin.ts +293 -0
- package/src/lib/resource-monitor/platform/index.ts +35 -0
- package/src/lib/resource-monitor/platform/linux.ts +332 -0
- package/src/lib/resource-monitor/platform/windows.ts +298 -0
- package/src/lib/resource-monitor/snapshot.ts +204 -0
- package/src/lib/resource-monitor/types.ts +74 -0
- package/src/lib/session-recorder/errors.ts +102 -0
- package/src/lib/session-recorder/flows/login.ts +210 -0
- package/src/lib/session-recorder/index.ts +361 -0
- package/src/lib/session-recorder/playwright.ts +257 -0
- package/src/lib/session-recorder/report.ts +353 -0
- package/src/lib/session-recorder/server.ts +267 -0
- package/src/lib/session-recorder/types.ts +115 -0
- package/src/plugin.ts +154 -15
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { AuthenticationFailed, PopupNotDetected } from "../errors";
|
|
3
|
+
import {
|
|
4
|
+
type BrowserHandle,
|
|
5
|
+
clickElement,
|
|
6
|
+
getBrowserMetrics,
|
|
7
|
+
navigateTo,
|
|
8
|
+
sleep,
|
|
9
|
+
waitForPopup,
|
|
10
|
+
waitForSelector,
|
|
11
|
+
} from "../playwright";
|
|
12
|
+
import type { SessionEventType } from "../types";
|
|
13
|
+
|
|
14
|
+
interface FlowRecorder {
|
|
15
|
+
recordEvent: (
|
|
16
|
+
type: SessionEventType,
|
|
17
|
+
label: string,
|
|
18
|
+
metadata?: Record<string, unknown>
|
|
19
|
+
) => Effect.Effect<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LoginFlowOptions {
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
headless: boolean;
|
|
25
|
+
stubWallet: boolean;
|
|
26
|
+
timeout: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_LOGIN_OPTIONS: LoginFlowOptions = {
|
|
30
|
+
baseUrl: "http://localhost:3000",
|
|
31
|
+
headless: true,
|
|
32
|
+
stubWallet: true,
|
|
33
|
+
timeout: 30000,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const runLoginFlow = (
|
|
37
|
+
browser: BrowserHandle,
|
|
38
|
+
recorder: FlowRecorder,
|
|
39
|
+
options: Partial<LoginFlowOptions> = {}
|
|
40
|
+
): Effect.Effect<void, AuthenticationFailed | PopupNotDetected> =>
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const opts = { ...DEFAULT_LOGIN_OPTIONS, ...options };
|
|
43
|
+
const { page, context } = browser;
|
|
44
|
+
|
|
45
|
+
yield* Effect.logInfo("Starting login flow");
|
|
46
|
+
yield* Effect.asVoid(recorder.recordEvent("custom", "login_flow_start"));
|
|
47
|
+
|
|
48
|
+
yield* navigateTo(page, `${opts.baseUrl}/login`);
|
|
49
|
+
yield* Effect.asVoid(recorder.recordEvent("pageload", "/login", { url: `${opts.baseUrl}/login` }));
|
|
50
|
+
|
|
51
|
+
yield* sleep(1000);
|
|
52
|
+
|
|
53
|
+
const connectButtonSelector = 'button:has-text("connect near wallet")';
|
|
54
|
+
|
|
55
|
+
yield* Effect.tryPromise({
|
|
56
|
+
try: () => page.waitForSelector(connectButtonSelector, { timeout: opts.timeout }),
|
|
57
|
+
catch: () => new AuthenticationFailed({
|
|
58
|
+
step: "find_connect_button",
|
|
59
|
+
reason: "Connect wallet button not found",
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
yield* Effect.asVoid(recorder.recordEvent("auth_start", "wallet_connect_initiated"));
|
|
64
|
+
|
|
65
|
+
if (opts.headless && opts.stubWallet) {
|
|
66
|
+
yield* Effect.logInfo("Stubbing wallet connection in headless mode");
|
|
67
|
+
yield* Effect.asVoid(recorder.recordEvent("custom", "wallet_stubbed", { reason: "headless_mode" }));
|
|
68
|
+
|
|
69
|
+
yield* clickElement(page, connectButtonSelector);
|
|
70
|
+
|
|
71
|
+
yield* sleep(2000);
|
|
72
|
+
|
|
73
|
+
const hasSignInButton = yield* Effect.tryPromise({
|
|
74
|
+
try: async () => {
|
|
75
|
+
try {
|
|
76
|
+
await page.waitForSelector('button:has-text("sign in as")', { timeout: 5000 });
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
catch: () => false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (hasSignInButton) {
|
|
86
|
+
yield* Effect.asVoid(recorder.recordEvent("custom", "sign_in_button_appeared"));
|
|
87
|
+
} else {
|
|
88
|
+
yield* Effect.asVoid(recorder.recordEvent("custom", "wallet_popup_would_open", {
|
|
89
|
+
note: "In headed mode, NEAR wallet popup would appear here",
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
yield* Effect.asVoid(recorder.recordEvent("auth_complete", "login_flow_completed_stubbed"));
|
|
94
|
+
|
|
95
|
+
} else {
|
|
96
|
+
yield* Effect.logInfo("Running full wallet flow (headed mode)");
|
|
97
|
+
|
|
98
|
+
yield* clickElement(page, connectButtonSelector);
|
|
99
|
+
yield* Effect.asVoid(recorder.recordEvent("click", "connect_wallet_button"));
|
|
100
|
+
|
|
101
|
+
const popupPromise = waitForPopup(context, opts.timeout);
|
|
102
|
+
|
|
103
|
+
const popup = yield* Effect.catchAll(popupPromise, () =>
|
|
104
|
+
Effect.fail(new PopupNotDetected({ timeoutMs: opts.timeout }))
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
yield* Effect.asVoid(recorder.recordEvent("popup_open", "near_wallet_popup", {
|
|
108
|
+
url: popup.url(),
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
yield* Effect.logInfo("NEAR wallet popup opened - waiting for user interaction");
|
|
112
|
+
yield* Effect.asVoid(recorder.recordEvent("custom", "awaiting_user_approval"));
|
|
113
|
+
|
|
114
|
+
yield* sleep(5000);
|
|
115
|
+
|
|
116
|
+
const popupClosed = yield* Effect.tryPromise({
|
|
117
|
+
try: async () => {
|
|
118
|
+
try {
|
|
119
|
+
await popup.waitForSelector("body", { timeout: 1000 });
|
|
120
|
+
return false;
|
|
121
|
+
} catch {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
catch: () => true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (popupClosed) {
|
|
129
|
+
yield* Effect.asVoid(recorder.recordEvent("popup_close", "near_wallet_popup"));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const signInButtonSelector = 'button:has-text("sign in as")';
|
|
133
|
+
const hasSignIn = yield* Effect.tryPromise({
|
|
134
|
+
try: async () => {
|
|
135
|
+
try {
|
|
136
|
+
await page.waitForSelector(signInButtonSelector, { timeout: 10000 });
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
catch: () => false,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (hasSignIn) {
|
|
146
|
+
yield* Effect.asVoid(recorder.recordEvent("custom", "wallet_connected"));
|
|
147
|
+
yield* clickElement(page, signInButtonSelector);
|
|
148
|
+
yield* Effect.asVoid(recorder.recordEvent("click", "sign_in_button"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
yield* Effect.asVoid(recorder.recordEvent("auth_complete", "login_flow_completed"));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
yield* Effect.logInfo("Login flow completed");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export const runNavigationFlow = (
|
|
158
|
+
browser: BrowserHandle,
|
|
159
|
+
recorder: FlowRecorder,
|
|
160
|
+
routes: string[],
|
|
161
|
+
baseUrl: string
|
|
162
|
+
): Effect.Effect<void> =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
yield* Effect.logInfo(`Running navigation flow through ${routes.length} routes`);
|
|
165
|
+
|
|
166
|
+
for (const route of routes) {
|
|
167
|
+
const url = `${baseUrl}${route}`;
|
|
168
|
+
yield* navigateTo(browser.page, url);
|
|
169
|
+
yield* Effect.asVoid(recorder.recordEvent("navigation", route, { url }));
|
|
170
|
+
|
|
171
|
+
yield* sleep(1000);
|
|
172
|
+
|
|
173
|
+
yield* Effect.asVoid(recorder.recordEvent("interval", `visited_${route}`));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
yield* Effect.logInfo("Navigation flow completed");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const runClickFlow = (
|
|
180
|
+
browser: BrowserHandle,
|
|
181
|
+
recorder: FlowRecorder,
|
|
182
|
+
selectors: Array<{ selector: string; label: string }>
|
|
183
|
+
): Effect.Effect<void> =>
|
|
184
|
+
Effect.gen(function* () {
|
|
185
|
+
yield* Effect.logInfo(`Running click flow with ${selectors.length} interactions`);
|
|
186
|
+
|
|
187
|
+
for (const { selector, label } of selectors) {
|
|
188
|
+
const exists = yield* Effect.tryPromise({
|
|
189
|
+
try: async () => {
|
|
190
|
+
try {
|
|
191
|
+
await browser.page.waitForSelector(selector, { timeout: 5000 });
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
catch: () => false,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (exists) {
|
|
201
|
+
yield* clickElement(browser.page, selector);
|
|
202
|
+
yield* Effect.asVoid(recorder.recordEvent("click", label, { selector }));
|
|
203
|
+
yield* sleep(500);
|
|
204
|
+
} else {
|
|
205
|
+
yield* Effect.logWarning(`Selector not found: ${selector}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
yield* Effect.logInfo("Click flow completed");
|
|
210
|
+
});
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { Effect, Logger, LogLevel } from "effect";
|
|
2
|
+
import { createSnapshotWithPlatform, PlatformLive, runSilent } from "../resource-monitor";
|
|
3
|
+
import type { Snapshot } from "../resource-monitor";
|
|
4
|
+
import {
|
|
5
|
+
type SessionRecorderError,
|
|
6
|
+
SessionTimeout,
|
|
7
|
+
SnapshotFailed,
|
|
8
|
+
} from "./errors";
|
|
9
|
+
import {
|
|
10
|
+
type BrowserHandle,
|
|
11
|
+
closeBrowser,
|
|
12
|
+
getBrowserMetrics,
|
|
13
|
+
launchBrowser,
|
|
14
|
+
} from "./playwright";
|
|
15
|
+
import {
|
|
16
|
+
exportHTMLReport,
|
|
17
|
+
exportJSON,
|
|
18
|
+
formatEventTimeline,
|
|
19
|
+
formatReportSummary,
|
|
20
|
+
generateReport,
|
|
21
|
+
} from "./report";
|
|
22
|
+
import {
|
|
23
|
+
checkPortsAvailable,
|
|
24
|
+
shutdownServers,
|
|
25
|
+
startServers,
|
|
26
|
+
waitForPortFree,
|
|
27
|
+
} from "./server";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_SESSION_CONFIG,
|
|
30
|
+
type BrowserMetrics,
|
|
31
|
+
type SessionConfig,
|
|
32
|
+
type SessionEvent,
|
|
33
|
+
type SessionEventType,
|
|
34
|
+
type SessionReport,
|
|
35
|
+
type ServerOrchestrator,
|
|
36
|
+
} from "./types";
|
|
37
|
+
|
|
38
|
+
let eventCounter = 0;
|
|
39
|
+
|
|
40
|
+
const generateEventId = (): string => {
|
|
41
|
+
eventCounter += 1;
|
|
42
|
+
return `evt_${Date.now()}_${eventCounter}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const generateSessionId = (): string => {
|
|
46
|
+
return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class SessionRecorder {
|
|
50
|
+
private config: SessionConfig;
|
|
51
|
+
private sessionId: string;
|
|
52
|
+
private events: SessionEvent[] = [];
|
|
53
|
+
private startTime: number = 0;
|
|
54
|
+
private endTime: number = 0;
|
|
55
|
+
private intervalHandle: ReturnType<typeof setInterval> | null = null;
|
|
56
|
+
private serverOrchestrator: ServerOrchestrator | null = null;
|
|
57
|
+
private browserHandle: BrowserHandle | null = null;
|
|
58
|
+
private isRecording = false;
|
|
59
|
+
|
|
60
|
+
private constructor(config: SessionConfig) {
|
|
61
|
+
this.config = config;
|
|
62
|
+
this.sessionId = generateSessionId();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static create = (
|
|
66
|
+
config?: Partial<SessionConfig>
|
|
67
|
+
): Effect.Effect<SessionRecorder> =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const fullConfig = { ...DEFAULT_SESSION_CONFIG, ...config };
|
|
70
|
+
yield* Effect.logInfo(`Creating SessionRecorder: ${JSON.stringify(fullConfig)}`);
|
|
71
|
+
return new SessionRecorder(fullConfig);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
getConfig(): SessionConfig {
|
|
75
|
+
return { ...this.config };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getSessionId(): string {
|
|
79
|
+
return this.sessionId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getEvents(): SessionEvent[] {
|
|
83
|
+
return [...this.events];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
isActive(): boolean {
|
|
87
|
+
return this.isRecording;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
takeSnapshot(): Effect.Effect<Snapshot, SnapshotFailed> {
|
|
91
|
+
const self = this;
|
|
92
|
+
return Effect.gen(function* () {
|
|
93
|
+
const snapshot = yield* Effect.tryPromise({
|
|
94
|
+
try: () => runSilent(createSnapshotWithPlatform({ ports: self.config.ports })),
|
|
95
|
+
catch: (e) => new SnapshotFailed({ reason: String(e) }),
|
|
96
|
+
});
|
|
97
|
+
return snapshot;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
recordEvent(
|
|
102
|
+
type: SessionEventType,
|
|
103
|
+
label: string,
|
|
104
|
+
metadata?: Record<string, unknown>
|
|
105
|
+
): Effect.Effect<SessionEvent, SnapshotFailed> {
|
|
106
|
+
const self = this;
|
|
107
|
+
return Effect.gen(function* () {
|
|
108
|
+
const snapshot = yield* self.takeSnapshot();
|
|
109
|
+
|
|
110
|
+
let browserMetrics: BrowserMetrics | undefined;
|
|
111
|
+
if (self.browserHandle) {
|
|
112
|
+
const metricsResult = yield* Effect.either(
|
|
113
|
+
getBrowserMetrics(self.browserHandle.page)
|
|
114
|
+
);
|
|
115
|
+
if (metricsResult._tag === "Right") {
|
|
116
|
+
browserMetrics = metricsResult.right;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const event: SessionEvent = {
|
|
121
|
+
id: generateEventId(),
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
type,
|
|
124
|
+
label,
|
|
125
|
+
snapshot,
|
|
126
|
+
browserMetrics,
|
|
127
|
+
url: self.browserHandle?.page.url(),
|
|
128
|
+
metadata,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
self.events.push(event);
|
|
132
|
+
yield* Effect.logDebug(`Event recorded: ${type} - ${label}`);
|
|
133
|
+
|
|
134
|
+
return event;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
startServers(
|
|
139
|
+
mode: "start" | "dev" = "start"
|
|
140
|
+
): Effect.Effect<void, SessionRecorderError> {
|
|
141
|
+
const self = this;
|
|
142
|
+
return Effect.gen(function* () {
|
|
143
|
+
yield* Effect.logInfo(`Starting servers in ${mode} mode`);
|
|
144
|
+
|
|
145
|
+
const portsAvailable = yield* checkPortsAvailable(self.config.ports);
|
|
146
|
+
if (!portsAvailable) {
|
|
147
|
+
yield* Effect.logWarning("Some ports already in use - proceeding anyway");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const orchestrator = yield* startServers(mode, {
|
|
151
|
+
port: self.config.ports[0],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
self.serverOrchestrator = orchestrator;
|
|
155
|
+
yield* Effect.logInfo("Servers started successfully");
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
stopServers(): Effect.Effect<void> {
|
|
160
|
+
const self = this;
|
|
161
|
+
return Effect.gen(function* () {
|
|
162
|
+
if (self.serverOrchestrator) {
|
|
163
|
+
yield* shutdownServers(self.serverOrchestrator);
|
|
164
|
+
self.serverOrchestrator = null;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
launchBrowser(): Effect.Effect<BrowserHandle, SessionRecorderError> {
|
|
170
|
+
const self = this;
|
|
171
|
+
return Effect.gen(function* () {
|
|
172
|
+
yield* Effect.logInfo(`Launching browser (headless: ${self.config.headless})`);
|
|
173
|
+
|
|
174
|
+
const handle = yield* launchBrowser(self.config.headless);
|
|
175
|
+
self.browserHandle = handle;
|
|
176
|
+
|
|
177
|
+
return handle;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
closeBrowser(): Effect.Effect<void> {
|
|
182
|
+
const self = this;
|
|
183
|
+
return Effect.gen(function* () {
|
|
184
|
+
if (self.browserHandle) {
|
|
185
|
+
yield* closeBrowser(self.browserHandle);
|
|
186
|
+
self.browserHandle = null;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getBrowser(): BrowserHandle | null {
|
|
192
|
+
return this.browserHandle;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
startRecording(): Effect.Effect<void, SnapshotFailed> {
|
|
196
|
+
const self = this;
|
|
197
|
+
return Effect.gen(function* () {
|
|
198
|
+
if (self.isRecording) {
|
|
199
|
+
yield* Effect.logWarning("Already recording");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
self.isRecording = true;
|
|
204
|
+
self.startTime = Date.now();
|
|
205
|
+
self.events = [];
|
|
206
|
+
|
|
207
|
+
yield* Effect.logInfo("Starting session recording");
|
|
208
|
+
|
|
209
|
+
yield* self.recordEvent("baseline", "session_start");
|
|
210
|
+
|
|
211
|
+
if (self.config.snapshotIntervalMs > 0) {
|
|
212
|
+
self.intervalHandle = setInterval(() => {
|
|
213
|
+
Effect.runPromise(
|
|
214
|
+
self.recordEvent("interval", "auto_snapshot").pipe(
|
|
215
|
+
Effect.catchAll(() => Effect.void)
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
}, self.config.snapshotIntervalMs);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
yield* Effect.logInfo(`Recording started with ${self.config.snapshotIntervalMs}ms interval`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
stopRecording(): Effect.Effect<SessionReport, SnapshotFailed> {
|
|
226
|
+
const self = this;
|
|
227
|
+
return Effect.gen(function* () {
|
|
228
|
+
if (!self.isRecording) {
|
|
229
|
+
yield* Effect.logWarning("Not recording");
|
|
230
|
+
return generateReport(
|
|
231
|
+
self.sessionId,
|
|
232
|
+
self.config,
|
|
233
|
+
self.events,
|
|
234
|
+
self.startTime,
|
|
235
|
+
Date.now()
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (self.intervalHandle) {
|
|
240
|
+
clearInterval(self.intervalHandle);
|
|
241
|
+
self.intervalHandle = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
yield* self.recordEvent("custom", "session_end");
|
|
245
|
+
|
|
246
|
+
self.isRecording = false;
|
|
247
|
+
self.endTime = Date.now();
|
|
248
|
+
|
|
249
|
+
yield* Effect.logInfo("Recording stopped");
|
|
250
|
+
|
|
251
|
+
const report = generateReport(
|
|
252
|
+
self.sessionId,
|
|
253
|
+
self.config,
|
|
254
|
+
self.events,
|
|
255
|
+
self.startTime,
|
|
256
|
+
self.endTime
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return report;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
exportReport(
|
|
264
|
+
filepath: string,
|
|
265
|
+
format: "json" | "html" = "json"
|
|
266
|
+
): Effect.Effect<void, SessionRecorderError> {
|
|
267
|
+
const self = this;
|
|
268
|
+
return Effect.gen(function* () {
|
|
269
|
+
const report = generateReport(
|
|
270
|
+
self.sessionId,
|
|
271
|
+
self.config,
|
|
272
|
+
self.events,
|
|
273
|
+
self.startTime,
|
|
274
|
+
self.endTime || Date.now()
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (format === "html") {
|
|
278
|
+
yield* exportHTMLReport(report, filepath);
|
|
279
|
+
} else {
|
|
280
|
+
yield* exportJSON(report, filepath);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
generateReport(): SessionReport {
|
|
286
|
+
return generateReport(
|
|
287
|
+
this.sessionId,
|
|
288
|
+
this.config,
|
|
289
|
+
this.events,
|
|
290
|
+
this.startTime,
|
|
291
|
+
this.endTime || Date.now()
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
printSummary(): Effect.Effect<void> {
|
|
296
|
+
const self = this;
|
|
297
|
+
return Effect.sync(() => {
|
|
298
|
+
const report = self.generateReport();
|
|
299
|
+
console.log(formatReportSummary(report));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
printTimeline(): Effect.Effect<void> {
|
|
304
|
+
const self = this;
|
|
305
|
+
return Effect.sync(() => {
|
|
306
|
+
console.log(formatEventTimeline(self.events));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cleanup(): Effect.Effect<void> {
|
|
311
|
+
const self = this;
|
|
312
|
+
return Effect.gen(function* () {
|
|
313
|
+
yield* Effect.logInfo("Cleaning up session recorder");
|
|
314
|
+
|
|
315
|
+
if (self.intervalHandle) {
|
|
316
|
+
clearInterval(self.intervalHandle);
|
|
317
|
+
self.intervalHandle = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
yield* self.closeBrowser();
|
|
321
|
+
yield* self.stopServers();
|
|
322
|
+
|
|
323
|
+
yield* Effect.logInfo("Cleanup complete");
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const runSession = <E>(
|
|
329
|
+
effect: Effect.Effect<void, E>
|
|
330
|
+
): Promise<void> =>
|
|
331
|
+
effect.pipe(
|
|
332
|
+
Effect.provide(PlatformLive),
|
|
333
|
+
Logger.withMinimumLogLevel(LogLevel.Info),
|
|
334
|
+
Effect.runPromise
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
export const runSessionSilent = <E>(
|
|
338
|
+
effect: Effect.Effect<void, E>
|
|
339
|
+
): Promise<void> =>
|
|
340
|
+
effect.pipe(
|
|
341
|
+
Effect.provide(PlatformLive),
|
|
342
|
+
Logger.withMinimumLogLevel(LogLevel.Error),
|
|
343
|
+
Effect.runPromise
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
export const runSessionDebug = <E>(
|
|
347
|
+
effect: Effect.Effect<void, E>
|
|
348
|
+
): Promise<void> =>
|
|
349
|
+
effect.pipe(
|
|
350
|
+
Effect.provide(PlatformLive),
|
|
351
|
+
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
352
|
+
Effect.runPromise
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
export * from "./errors";
|
|
356
|
+
export * from "./types";
|
|
357
|
+
export * from "./server";
|
|
358
|
+
export * from "./playwright";
|
|
359
|
+
export * from "./report";
|
|
360
|
+
export { runLoginFlow, runNavigationFlow, runClickFlow } from "./flows/login";
|
|
361
|
+
export { diffSnapshots, hasLeaks, type Snapshot, type SnapshotDiff } from "../resource-monitor";
|