Skip to content

Commit b5f9d49

Browse files
osho-20claude
andcommitted
fix(observability): inject TRA support-file listener synchronously (SDK-7121)
setEventListeners deferred the support-file write to an async glob callback while runs.js proceeded synchronously to md5 hashing and zip archiving. The injection raced the archive: a lost race shipped an un-instrumented suite, and md5 caching ("Skipping zip upload...") made the bad zip sticky, so the new Automate dashboard (TRA) received zero test events while the old dashboard (independent of the injected plugin) kept working. This is why the symptom was environment-specific — a slower CI pipeline loses the race. Switch setEventListeners (and the identical race in the accessibility setAccessibilityEventListeners glob branch) to glob.sync so the writes complete before the caller reads the files. Adds a regression test asserting the observability require is present synchronously after the call returns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b889f91 commit b5f9d49

3 files changed

Lines changed: 88 additions & 43 deletions

File tree

bin/accessibility-automation/helper.js

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -378,32 +378,29 @@ exports.setAccessibilityEventListeners = (bsConfig) => {
378378
}
379379

380380
const globPattern = process.cwd() + supportFilesData.supportFile;
381-
glob(globPattern, {}, (err, files) => {
382-
if(err) {
383-
logger.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files');
384-
return;
385-
}
386-
387-
files.forEach(file => {
388-
try {
389-
const fileName = path.basename(file);
390-
if(['e2e.js', 'e2e.ts', 'component.ts', 'component.js'].includes(fileName) && !file.includes('node_modules')) {
391-
392-
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
393-
let cypressCommandEventListener = getAccessibilityCypressCommandEventListener(path.extname(file));
394-
if(!defaultFileContent.includes(cypressCommandEventListener)) {
395-
let newFileContent = defaultFileContent +
396-
'\n' +
397-
cypressCommandEventListener +
398-
'\n';
399-
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
400-
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
401-
}
381+
// Synchronous for the same reason as testObservability setEventListeners
382+
// (SDK-7121): the caller archives the suite right after this returns, so an
383+
// async glob callback would race the archive and ship un-instrumented specs.
384+
const files = glob.sync(globPattern, {});
385+
files.forEach(file => {
386+
try {
387+
const fileName = path.basename(file);
388+
if(['e2e.js', 'e2e.ts', 'component.ts', 'component.js'].includes(fileName) && !file.includes('node_modules')) {
389+
390+
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
391+
let cypressCommandEventListener = getAccessibilityCypressCommandEventListener(path.extname(file));
392+
if(!defaultFileContent.includes(cypressCommandEventListener)) {
393+
let newFileContent = defaultFileContent +
394+
'\n' +
395+
cypressCommandEventListener +
396+
'\n';
397+
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
398+
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
402399
}
403-
} catch(e) {
404-
logger.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
405400
}
406-
});
401+
} catch(e) {
402+
logger.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
403+
}
407404
});
408405
} catch(e) {
409406
logger.debug(`Unable to parse support files to set event listeners with error ${e}`, true, e);

bin/testObservability/helper/helper.js

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -301,27 +301,29 @@ exports.setEventListeners = (bsConfig) => {
301301
try {
302302
const supportFilesData = helper.getSupportFiles(bsConfig, false);
303303
if(!supportFilesData.supportFile) return;
304-
glob(process.cwd() + supportFilesData.supportFile, {}, (err, files) => {
305-
if(err) return exports.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files');
306-
files.forEach(file => {
307-
try {
308-
if (isE2ESupportFile(file) || !files.some(f => isE2ESupportFile(f))) {
309-
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
310-
311-
let cypressCommandEventListener = getCypressCommandEventListener(file.includes('js'));
312-
if(!defaultFileContent.includes(cypressCommandEventListener)) {
313-
let newFileContent = defaultFileContent +
314-
'\n' +
315-
cypressCommandEventListener +
316-
'\n'
317-
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
318-
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
319-
}
304+
// Must be synchronous: runs.js proceeds to md5 hashing and zip archiving
305+
// immediately after this returns. An async glob callback races the archive
306+
// (SDK-7121) — a lost race ships an un-instrumented suite, and md5 caching
307+
// makes it sticky, so TRA receives no test events.
308+
const files = glob.sync(process.cwd() + supportFilesData.supportFile, {});
309+
files.forEach(file => {
310+
try {
311+
if (isE2ESupportFile(file) || !files.some(f => isE2ESupportFile(f))) {
312+
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
313+
314+
let cypressCommandEventListener = getCypressCommandEventListener(file.includes('js'));
315+
if(!defaultFileContent.includes(cypressCommandEventListener)) {
316+
let newFileContent = defaultFileContent +
317+
'\n' +
318+
cypressCommandEventListener +
319+
'\n'
320+
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
321+
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
320322
}
321-
} catch(e) {
322-
exports.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
323323
}
324-
});
324+
} catch(e) {
325+
exports.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
326+
}
325327
});
326328
} catch(e) {
327329
exports.debug(`Unable to parse support files to set event listeners with error ${e}`, true, e);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
const chai = require('chai');
3+
const expect = chai.expect;
4+
const sinon = require('sinon');
5+
const fs = require('fs');
6+
const os = require('os');
7+
const path = require('path');
8+
9+
const o11yHelper = require('../../../../bin/testObservability/helper/helper');
10+
const baseHelper = require('../../../../bin/helpers/helper');
11+
12+
// Regression guard for SDK-7121: the TRA support-file injection MUST land
13+
// synchronously. runs.js calls setEventListeners(bsConfig) and then proceeds
14+
// immediately to md5 hashing + zip archiving. When the injection was deferred to
15+
// an async glob callback, it raced the archive — a lost race shipped an
16+
// un-instrumented suite, and md5 caching made it sticky, so the new Automate
17+
// dashboard (TRA) received zero test events.
18+
describe('testObservability setEventListeners', () => {
19+
let tmpDir, cwdStub, getSupportFilesStub;
20+
21+
beforeEach(() => {
22+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk7121-'));
23+
fs.mkdirSync(path.join(tmpDir, 'cypress', 'support'), { recursive: true });
24+
fs.writeFileSync(path.join(tmpDir, 'cypress', 'support', 'e2e.js'), '// user original support file\n');
25+
26+
cwdStub = sinon.stub(process, 'cwd').returns(tmpDir);
27+
getSupportFilesStub = sinon.stub(baseHelper, 'getSupportFiles').returns({
28+
supportFile: '/cypress/support/e2e.js',
29+
cleanupParams: {}
30+
});
31+
});
32+
33+
afterEach(() => {
34+
cwdStub.restore();
35+
getSupportFilesStub.restore();
36+
fs.rmSync(tmpDir, { recursive: true, force: true });
37+
});
38+
39+
it('injects the observability require synchronously before returning', () => {
40+
o11yHelper.setEventListeners({ run_settings: {} });
41+
42+
// Read exactly as md5/archive would — synchronously, right after the call.
43+
const content = fs.readFileSync(path.join(tmpDir, 'cypress', 'support', 'e2e.js'), 'utf-8');
44+
expect(content).to.include("browserstack-cypress-cli/bin/testObservability/cypress");
45+
});
46+
});

0 commit comments

Comments
 (0)