Skip to content

Commit 254a69d

Browse files
fix(node18): make telemetry fail-safe + openAsBlob fallback + scope fake timers
CI's Node 18.20 job (supported per engines: node >=18.20.0; wdio v9 supports 18.20) surfaced three Node-18-only failures that Node 20/22 tolerate: 1. PerformanceTester.start/end/measure called performance.mark/measure directly. Under vitest's full fake timers (7 test files) performance.now() goes negative, and Node 18's perf_hooks rejects negative timestamps (ERR_PERFORMANCE_INVALID_TIMESTAMP), throwing out of callers' before() hooks. Routed all mark/measure through safeMark/safeMeasure wrappers — instrumentation must never throw into business logic (no-op on the happy path; transparent on Node 20+). 2. uploadLogs used fs.openAsBlob (Node >=20 only). Added a Node-18 fallback to new Blob([readFileSync]) for the small log archive. 3. util.test.ts faked via bare useFakeTimers(); scoped to toFake:['Date'] (it never advances timers), matching reporter.test.ts. Verified locally on Node 18.20.5 AND Node 22: npm ci + build + 980/980 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 67b9cef commit 254a69d

3 files changed

Lines changed: 36 additions & 16 deletions

File tree

packages/browserstack-service/src/instrumentation/performance/performance-tester.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ import type { CsvWriter } from 'csv-writer/src/lib/csv-writer.js'
1818
import type { ObjectMap } from 'csv-writer/src/lib/lang/object.js'
1919
import type { Browser } from 'webdriverio'
2020

21+
// Telemetry must never throw into business logic. Node 18's perf_hooks rejects
22+
// negative timestamps (e.g. performance.now() under faked timers, or any other
23+
// edge) with ERR_PERFORMANCE_INVALID_TIMESTAMP; Node 20+ tolerates them. Wrapping
24+
// mark/measure keeps an instrumentation failure from breaking a caller's hook
25+
// (e.g. a service before()), and is a no-op on the happy path.
26+
function safeMark(markName: string) {
27+
try { performance.mark(markName) } catch { /* instrumentation must not throw */ }
28+
}
29+
function safeMeasure(measureName: string, start: string, end: string) {
30+
try { performance.measure(measureName, start, end) } catch { /* instrumentation must not throw */ }
31+
}
32+
2133
type PerformanceDetails = {
2234
success?: true,
2335
failure?: string,
@@ -224,7 +236,7 @@ export default class PerformanceTester {
224236
const endMark = `${label}-end-${uniqueId}`
225237

226238
// Create the start mark with unique ID
227-
performance.mark(startMark)
239+
safeMark(startMark)
228240

229241
// Store details with measurement context
230242
const detailsWithContext = {
@@ -240,8 +252,8 @@ export default class PerformanceTester {
240252
returnVal
241253
.then(v => {
242254
// Use specific marks for this call
243-
performance.mark(endMark)
244-
performance.measure(label, startMark, endMark)
255+
safeMark(endMark)
256+
safeMeasure(label, startMark, endMark)
245257

246258
this.details[label] = Object.assign({
247259
success: true,
@@ -255,8 +267,8 @@ export default class PerformanceTester {
255267

256268
resolve(v)
257269
}).catch(e => {
258-
performance.mark(endMark)
259-
performance.measure(label, startMark, endMark)
270+
safeMark(endMark)
271+
safeMeasure(label, startMark, endMark)
260272

261273
this.details[label] = Object.assign({
262274
success: false,
@@ -274,8 +286,8 @@ export default class PerformanceTester {
274286
}
275287

276288
// Synchronous execution
277-
performance.mark(endMark)
278-
performance.measure(label, startMark, endMark)
289+
safeMark(endMark)
290+
safeMeasure(label, startMark, endMark)
279291

280292
this.details[label] = Object.assign({
281293
success: true,
@@ -289,8 +301,8 @@ export default class PerformanceTester {
289301

290302
return returnVal
291303
} catch (er) {
292-
performance.mark(endMark)
293-
performance.measure(label, startMark, endMark)
304+
safeMark(endMark)
305+
safeMeasure(label, startMark, endMark)
294306

295307
this.details[label] = Object.assign({
296308
success: false,
@@ -309,13 +321,13 @@ export default class PerformanceTester {
309321
static start(event: string) {
310322
const finalEvent = event + '-start'
311323
if (this.eventsMap[finalEvent]) {return}
312-
performance.mark(finalEvent)
324+
safeMark(finalEvent)
313325
this.eventsMap[finalEvent] = 1
314326
}
315327

316328
static end(event: string, success = true, failure?: string | unknown, details = {}) {
317-
performance.mark(event + '-end')
318-
performance.measure(event, event + '-start', event + '-end')
329+
safeMark(event + '-end')
330+
safeMeasure(event, event + '-start', event + '-end')
319331
// Clear the start-mark guard so a subsequent start(event) for the same
320332
// event actually marks a new start. Without this, start() short-circuits
321333
// and the next end() measures from the original start mark — inflated

packages/browserstack-service/src/util.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,10 +1454,14 @@ export async function uploadLogs(user: string | undefined, key: string | undefin
14541454
})
14551455

14561456
const formData = new FormData()
1457-
// openAsBlob returns a Blob backed by a file descriptor — undici streams
1458-
// from disk during the upload instead of materialising the full archive
1457+
// openAsBlob (Node >=20) returns a Blob backed by a file descriptor — undici
1458+
// streams from disk during the upload instead of materialising the full archive
14591459
// in V8 heap (which readFileSync + new Blob([Buffer]) would do twice).
1460-
const file = await fs.openAsBlob(tarGzPath, { type: 'application/x-gzip' })
1460+
// Node 18.20 (still supported per `engines`) lacks fs.openAsBlob, so fall back
1461+
// to reading the (small) log archive into a Blob there.
1462+
const file = typeof fs.openAsBlob === 'function'
1463+
? await fs.openAsBlob(tarGzPath, { type: 'application/x-gzip' })
1464+
: new Blob([fs.readFileSync(tarGzPath)], { type: 'application/x-gzip' })
14611465
formData.append('data', file, 'logs.tar.gz')
14621466
formData.append('clientBuildUuid', clientBuildUuid)
14631467

packages/browserstack-service/tests/util.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ const log = logger('test')
6868

6969
vi.mock('fetch')
7070
vi.mock('git-repo-info')
71-
vi.useFakeTimers().setSystemTime(new Date('2020-01-01'))
71+
// Fake only Date (not `performance`): these tests need a deterministic system
72+
// clock but never advance timers. Faking `performance` too makes performance.now()
73+
// negative under the 2020 system time, which Node 18's perf_hooks rejects with
74+
// ERR_PERFORMANCE_INVALID_TIMESTAMP (Node 20+ tolerates it). Mirrors reporter.test.ts.
75+
vi.useFakeTimers({ toFake: ['Date'] }).setSystemTime(new Date('2020-01-01'))
7276
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
7377

7478
// Mock testHub utilities

0 commit comments

Comments
 (0)