Skip to content

Commit 8d5d2a1

Browse files
committed
update redraw
1 parent f9053f7 commit 8d5d2a1

File tree

9 files changed

+548
-53
lines changed

9 files changed

+548
-53
lines changed

agent/lib/commands.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ const createCommands = (
469469
if (action !== "hover") {
470470
// Update timestamp for the actual click action
471471
elementData.timestamp = Date.now();
472+
472473

473474
if (action === "click" || action === "left-click") {
474475
await sandbox.send({ type: "leftClick", x, y, ...elementData });
@@ -928,7 +929,7 @@ const createCommands = (
928929
interactionType: "wait",
929930
session: sessionId,
930931
input: { timeout },
931-
timestamp: waitTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
932+
timestamp: waitTimestamp, // Use dashcam elapsed time instead of absolute time
932933
duration: waitDuration,
933934
success: true,
934935
});

agent/lib/redraw.js

Lines changed: 96 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const DEFAULT_REDRAW_OPTIONS = {
88
enabled: true, // Master switch to enable/disable redraw detection
99
screenRedraw: true, // Enable screen redraw detection
1010
networkMonitor: true, // Enable network activity monitoring
11-
diffThreshold: 0.1, // Percentage threshold for screen diff (0.01 = 0.01%)
1211
};
1312

1413
// Factory function that creates redraw functionality with the provided system instance
@@ -20,10 +19,6 @@ const createRedraw = (
2019
) => {
2120
// Merge default options with provided defaults
2221
const baseOptions = { ...DEFAULT_REDRAW_OPTIONS, ...defaultOptions };
23-
// Support legacy redrawThresholdPercent number argument
24-
if (typeof defaultOptions === 'number') {
25-
baseOptions.diffThreshold = defaultOptions;
26-
}
2722

2823
const networkUpdateInterval = 15000;
2924

@@ -35,7 +30,13 @@ const createRedraw = (
3530

3631
let measurements = [];
3732
let networkSettled = true;
38-
let screenHasRedrawn = null;
33+
34+
// Screen stability tracking
35+
let initialScreenImage = null; // The image captured at start() - reference point
36+
let lastScreenImage = null; // Previous frame for consecutive comparison
37+
let hasChangedFromInitial = false; // Has screen changed from initial state?
38+
let consecutiveFramesStable = false; // Are consecutive frames now stable?
39+
let screenMeasurements = []; // Track consecutive frame diffs for stability detection
3940

4041
// Track network interval to ensure only one exists
4142
let networkInterval = null;
@@ -45,7 +46,11 @@ const createRedraw = (
4546
lastRxBytes = null;
4647
measurements = [];
4748
networkSettled = true;
48-
screenHasRedrawn = false;
49+
initialScreenImage = null;
50+
lastScreenImage = null;
51+
hasChangedFromInitial = false;
52+
consecutiveFramesStable = false;
53+
screenMeasurements = [];
4954
};
5055

5156
const parseNetworkStats = (thisRxBytes, thisTxBytes) => {
@@ -85,6 +90,40 @@ const createRedraw = (
8590
}
8691
};
8792

93+
// Parse screen diff stats for consecutive frame stability
94+
// Detects when consecutive frames have stopped changing
95+
const parseConsecutiveDiffStats = (diffPercent) => {
96+
screenMeasurements.push(diffPercent);
97+
98+
// Keep last 10 measurements for stability detection
99+
if (screenMeasurements.length > 10) {
100+
screenMeasurements.shift();
101+
}
102+
103+
// Need at least 2 measurements to determine stability
104+
if (screenMeasurements.length < 2) {
105+
consecutiveFramesStable = false;
106+
return;
107+
}
108+
109+
let avgDiff = screenMeasurements.reduce((acc, d) => acc + d, 0) / screenMeasurements.length;
110+
111+
let stdDevDiff = Math.sqrt(
112+
screenMeasurements.reduce((acc, d) => acc + Math.pow(d - avgDiff, 2), 0) /
113+
screenMeasurements.length,
114+
);
115+
116+
let zIndexDiff = stdDevDiff !== 0 ? (diffPercent - avgDiff) / stdDevDiff : 0;
117+
118+
// Consecutive frames are stable when z-index is negative (current diff is below average)
119+
// or diff is essentially zero (< 0.1% accounts for compression artifacts)
120+
if (screenMeasurements.length >= 2 && (diffPercent < 0.1 || zIndexDiff < 0)) {
121+
consecutiveFramesStable = true;
122+
} else {
123+
consecutiveFramesStable = false;
124+
}
125+
};
126+
88127
async function updateNetwork() {
89128
if (sandbox && sandbox.instanceSocketConnected) {
90129
let network = await sandbox.send({
@@ -141,8 +180,6 @@ const createRedraw = (
141180
}
142181
}
143182

144-
let startImage = null;
145-
146183
// Start network monitoring only when needed
147184
function startNetworkMonitoring() {
148185
if (!networkInterval) {
@@ -187,17 +224,18 @@ const createRedraw = (
187224
startNetworkMonitoring();
188225
}
189226

190-
// Only capture start image if screen redraw is enabled
227+
// Capture initial image for screen stability monitoring
191228
if (currentOptions.screenRedraw) {
192-
startImage = await system.captureScreenPNG(0.25, true);
193-
emitter.emit(events.log.debug, `[redraw] start() - captured startImage: ${startImage}`);
229+
initialScreenImage = await system.captureScreenPNG(0.25, true);
230+
lastScreenImage = initialScreenImage;
231+
emitter.emit(events.log.debug, `[redraw] start() - captured initial image: ${initialScreenImage}`);
194232
}
195233

196-
return startImage;
234+
return initialScreenImage;
197235
}
198236

199237
async function checkCondition(resolve, startTime, timeoutMs, options) {
200-
const { enabled, screenRedraw, networkMonitor, diffThreshold } = options;
238+
const { enabled, screenRedraw, networkMonitor } = options;
201239

202240
// If redraw is disabled, resolve immediately
203241
if (!enabled) {
@@ -207,33 +245,51 @@ const createRedraw = (
207245

208246
let nowImage = screenRedraw ? await system.captureScreenPNG(0.25, true) : null;
209247
let timeElapsed = Date.now() - startTime;
210-
let diffPercent = 0;
248+
let diffFromInitial = 0;
249+
let diffFromLast = 0;
211250
let isTimeout = timeElapsed > timeoutMs;
212251

213-
// Check screen redraw if enabled and we have a start image to compare against
214-
if (screenRedraw && !screenHasRedrawn && startImage && nowImage) {
215-
emitter.emit(events.log.debug, `[redraw] checkCondition() - comparing images: ${JSON.stringify({ startImage, nowImage })}`);
216-
diffPercent = await imageDiffPercent(startImage, nowImage);
217-
emitter.emit(events.log.debug, `[redraw] checkCondition() - diffPercent: ${diffPercent}, threshold: ${diffThreshold}`);
218-
screenHasRedrawn = diffPercent > diffThreshold;
219-
emitter.emit(events.log.debug, `[redraw] checkCondition() - screenHasRedrawn: ${screenHasRedrawn}`);
220-
} else if (screenRedraw && !startImage) {
221-
// If no start image was captured, capture one now and wait for next check
222-
emitter.emit(events.log.debug, '[redraw] checkCondition() - no startImage, capturing now');
223-
startImage = await system.captureScreenPNG(0.25, true);
252+
// Screen stability detection:
253+
// 1. Check if screen has changed from initial (detect transition)
254+
// 2. Check if consecutive frames are stable (detect settling)
255+
if (screenRedraw && nowImage) {
256+
// Compare to initial image - has the screen changed at all?
257+
if (initialScreenImage && !hasChangedFromInitial) {
258+
diffFromInitial = await imageDiffPercent(initialScreenImage, nowImage);
259+
emitter.emit(events.log.debug, `[redraw] checkCondition() - diffFromInitial: ${diffFromInitial}`);
260+
// Consider changed if diff > 0.1% (accounts for compression artifacts)
261+
if (diffFromInitial > 0.1) {
262+
hasChangedFromInitial = true;
263+
emitter.emit(events.log.debug, `[redraw] checkCondition() - screen has changed from initial!`);
264+
}
265+
}
266+
267+
// Compare consecutive frames - has the screen stopped changing?
268+
if (lastScreenImage && lastScreenImage !== initialScreenImage) {
269+
diffFromLast = await imageDiffPercent(lastScreenImage, nowImage);
270+
emitter.emit(events.log.debug, `[redraw] checkCondition() - diffFromLast: ${diffFromLast}`);
271+
parseConsecutiveDiffStats(diffFromLast);
272+
emitter.emit(events.log.debug, `[redraw] checkCondition() - consecutiveFramesStable: ${consecutiveFramesStable}, measurements: ${screenMeasurements.length}`);
273+
}
274+
275+
// Update last image for next comparison
276+
lastScreenImage = nowImage;
224277
}
225278

226-
// If screen redraw is disabled, consider it as "redrawn"
227-
const effectiveScreenRedrawn = screenRedraw ? screenHasRedrawn : true;
279+
// Screen is settled when: it has changed from initial AND consecutive frames are now stable
280+
const screenSettled = hasChangedFromInitial && consecutiveFramesStable;
281+
282+
// If screen redraw is disabled, consider it as "settled"
283+
const effectiveScreenSettled = screenRedraw ? screenSettled : true;
228284
// If network monitor is disabled, consider it as "settled"
229285
const effectiveNetworkSettled = networkMonitor ? networkSettled : true;
230286

231-
// Log redraw status
287+
// Log redraw status - show both change detection and stability
232288
let redrawText = !screenRedraw
233289
? theme.dim(`disabled`)
234-
: effectiveScreenRedrawn
290+
: effectiveScreenSettled
235291
? theme.green(`y`)
236-
: theme.dim(`${diffPercent}/${diffThreshold}%`);
292+
: theme.dim(`${hasChangedFromInitial ? '✓' : '?'}${consecutiveFramesStable ? '✓' : diffFromLast.toFixed(1)}%`);
237293
let networkText = !networkMonitor
238294
? theme.dim(`disabled`)
239295
: effectiveNetworkSettled
@@ -248,9 +304,11 @@ const createRedraw = (
248304
emitter.emit(events.redraw.status, {
249305
redraw: {
250306
enabled: screenRedraw,
251-
hasRedrawn: effectiveScreenRedrawn,
252-
diffPercent,
253-
threshold: diffThreshold,
307+
settled: effectiveScreenSettled,
308+
hasChangedFromInitial,
309+
consecutiveFramesStable,
310+
diffFromInitial,
311+
diffFromLast,
254312
text: redrawText,
255313
},
256314
network: {
@@ -268,9 +326,11 @@ const createRedraw = (
268326
},
269327
});
270328

271-
if ((effectiveScreenRedrawn && effectiveNetworkSettled) || isTimeout) {
329+
if ((effectiveScreenSettled && effectiveNetworkSettled) || isTimeout) {
272330
emitter.emit(events.redraw.complete, {
273-
screenHasRedrawn: effectiveScreenRedrawn,
331+
screenSettled: effectiveScreenSettled,
332+
hasChangedFromInitial,
333+
consecutiveFramesStable,
274334
networkSettled: effectiveNetworkSettled,
275335
isTimeout,
276336
timeElapsed,

agent/lib/sandbox.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
3636
this.uniqueId = Math.random().toString(36).substring(7);
3737
this.os = null; // Store OS value to send with every message
3838
this.sessionInstance = sessionInstance; // Store session instance to include in messages
39+
this.traceId = null; // Sentry trace ID for debugging
40+
}
41+
42+
/**
43+
* Get the Sentry trace ID for this session
44+
* Useful for debugging with customers - they can share this ID to look up their traces
45+
* @returns {string|null} The trace ID or null if not authenticated
46+
*/
47+
getTraceId() {
48+
return this.traceId;
49+
}
50+
51+
/**
52+
* Get the Sentry trace URL for this session
53+
* @returns {string|null} The full Sentry trace URL or null if no trace ID
54+
*/
55+
getTraceUrl() {
56+
if (!this.traceId) return null;
57+
return `https://testdriver.sentry.io/trace/${this.traceId}`;
3958
}
4059

4160
send(message, timeout = 300000) {
@@ -122,7 +141,15 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
122141

123142
if (reply.success) {
124143
this.authenticated = true;
125-
emitter.emit(events.sandbox.authenticated);
144+
145+
// Log and store the Sentry trace ID for debugging
146+
if (reply.traceId) {
147+
this.traceId = reply.traceId;
148+
console.log(`[Sandbox] Sentry Trace ID: ${reply.traceId}`);
149+
console.log(`[Sandbox] View trace: https://testdriver.sentry.io/trace/${reply.traceId}`);
150+
}
151+
152+
emitter.emit(events.sandbox.authenticated, { traceId: reply.traceId });
126153
return true;
127154
}
128155
}

docs/v7/features/stable.mdx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ await testdriver.find('element', {
102102
}
103103
}).click();
104104

105-
// Adjust screen diff sensitivity
106-
await testdriver.find('animated button', {
105+
// Disable only screen monitoring (keep network monitoring)
106+
await testdriver.find('animated element', {
107107
redraw: {
108-
diffThreshold: 0.05 // Higher threshold = less sensitive to changes
108+
screenRedraw: false // Skip screen stability, still wait for network
109109
}
110110
}).click();
111111
```
@@ -299,9 +299,8 @@ const testdriver = new TestDriver({
299299
apiKey: process.env.TD_API_KEY,
300300
redraw: {
301301
enabled: true, // Master switch for redraw detection
302-
screenRedraw: true, // Enable screen change detection
302+
screenRedraw: true, // Enable screen stability detection
303303
networkMonitor: true, // Enable network activity monitoring
304-
diffThreshold: 0.01, // Screen diff threshold (0.01 = 0.01%)
305304
}
306305
});
307306
```
@@ -312,14 +311,14 @@ const testdriver = new TestDriver({
312311
// Override redraw settings for specific actions
313312
await testdriver.find('fast element', {
314313
redraw: {
315-
enabled: false // Skip redraw detection
314+
enabled: false // Skip redraw detection entirely
316315
}
317316
}).click();
318317

319-
await testdriver.find('element with subtle changes', {
318+
await testdriver.find('element with animation', {
320319
redraw: {
321-
diffThreshold: 0.05, // Less sensitive to screen changes
322-
networkMonitor: false // Skip network monitoring
320+
screenRedraw: false, // Skip screen stability check
321+
networkMonitor: true // Still wait for network to settle
323322
}
324323
}).click();
325324
```
@@ -337,7 +336,7 @@ const testdriver = new TestDriver({
337336

338337
await testdriver.find('element').click();
339338
// Console output will show:
340-
// [redraw] Screen change detected: 0.15% (threshold: 0.01%)
339+
// [redraw] Screen diff: 0.15%
341340
// [redraw] Network activity: 1250 b/s
342341
// [redraw] Waiting for stability...
343342
```

interfaces/cli/lib/base.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ class BaseCommand extends Command {
7373
};
7474

7575
let isConnected = false;
76+
const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
7677

77-
// Use pattern matching for log events, but skip log:Debug
78+
// Use pattern matching for log events, but skip log:Debug unless debug mode is enabled
7879
this.agent.emitter.on("log:*", (message) => {
7980
const event = this.agent.emitter.event;
8081

81-
if (event === events.log.debug) return;
82+
if (event === events.log.debug && !debugMode) return;
8283

8384
if (event === events.log.narration && isConnected) return;
8485
console.log(message);

sdk.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2297,14 +2297,15 @@ class TestDriverSDK {
22972297
_setupLogging() {
22982298
// Track the last fatal error message to throw on exit
22992299
let lastFatalError = null;
2300+
const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
23002301

23012302
// Set up markdown logger
23022303
createMarkdownLogger(this.emitter);
23032304

23042305
// Set up basic event logging
23052306
this.emitter.on("log:**", (message) => {
23062307
const event = this.emitter.event;
2307-
if (event === events.log.debug) return;
2308+
if (event === events.log.debug && !debugMode) return;
23082309
if (this.loggingEnabled && message) {
23092310
const prefixedMessage = this.testContext
23102311
? `[${this.testContext}] ${message}`

0 commit comments

Comments
 (0)