From 2355d50ea33b24e9141db5ea39efb825bb25cee6 Mon Sep 17 00:00:00 2001
From: Saurav Das <saurav.d@browserstack.com>
Date: Fri, 14 Jun 2024 12:16:21 +0530
Subject: [PATCH] chore: Build unification for Cypress

---
 bin/accessibility-automation/cypress/index.js |  89 ++++----
 bin/accessibility-automation/plugin/index.js  |  21 ++
 bin/commands/runs.js                          |  13 +-
 bin/helpers/helper.js                         |  13 +-
 bin/helpers/utils.js                          |   3 +-
 bin/testObservability/helper/constants.js     |   3 +-
 bin/testObservability/helper/helper.js        |  38 ++--
 bin/testObservability/reporter/index.js       |  41 +++-
 bin/testhub/constants.js                      |  10 +
 bin/testhub/plugin/index.js                   |   9 +
 bin/testhub/testhubHandler.js                 | 119 ++++++++++
 bin/testhub/utils.js                          | 205 ++++++++++++++++++
 12 files changed, 494 insertions(+), 70 deletions(-)
 create mode 100644 bin/testhub/constants.js
 create mode 100644 bin/testhub/plugin/index.js
 create mode 100644 bin/testhub/testhubHandler.js
 create mode 100644 bin/testhub/utils.js

diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js
index 6bc8aa3b..0d863fa5 100644
--- a/bin/accessibility-automation/cypress/index.js
+++ b/bin/accessibility-automation/cypress/index.js
@@ -5,6 +5,10 @@ const browserStackLog = (message) => {
   cy.task('browserstack_log', message);
 }
 
+const sendToReporter = (data) => {
+  cy.task('test_accessibility_data', data);
+}
+
 const commandsToWrap = ['visit', 'click', 'type', 'request', 'dblclick', 'rightclick', 'clear', 'check', 'uncheck', 'select', 'trigger', 'selectFile', 'scrollIntoView', 'scroll', 'scrollTo', 'blur', 'focus', 'go', 'reload', 'submit', 'viewport', 'origin'];
 
 const performScan = (win, payloadToSend) =>
@@ -250,8 +254,11 @@ const shouldScanForAccessibility = (attributes) => {
       if (Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) {
         excludeTagArray = Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY").split(";")
       }
+      browserStackLog("EXCLUDE_TAGS_FOR_ACCESSIBILITY = " + excludeTagArray)
+      browserStackLog("INCLUDE_TAGS_FOR_ACCESSIBILITY = " + includeTagArray)
 
       const fullTestName = attributes.title;
+      browserStackLog("fullTestName = " + fullTestName)
       const excluded = excludeTagArray.some((exclude) => fullTestName.includes(exclude));
       const included = includeTagArray.length === 0 || includeTags.some((include) => fullTestName.includes(include));
       shouldScanTestForAccessibility = !excluded && included;
@@ -274,6 +281,7 @@ Cypress.on('command:start', async (command) => {
   const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest || Cypress.mocha.getRunner().suite.ctx._runnable;
 
   let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
+  sendToReporter({[attributes.title]: shouldScanTestForAccessibility})
   if (!shouldScanTestForAccessibility) return;
 
   cy.window().then((win) => {
@@ -284,47 +292,48 @@ Cypress.on('command:start', async (command) => {
 
 afterEach(() => {
   const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest;
-  cy.window().then(async (win) => {
-    let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
-    if (!shouldScanTestForAccessibility) return cy.wrap({});
-
-    cy.wrap(performScan(win), {timeout: 30000}).then(() => {
-      try {
-        let os_data;
-        if (Cypress.env("OS")) {
-          os_data = Cypress.env("OS");
-        } else {
-          os_data = Cypress.platform === 'linux' ? 'mac' : "win"
-        }
-        let filePath = '';
-        if (attributes.invocationDetails !== undefined && attributes.invocationDetails.relativeFile !== undefined) {
-          filePath = attributes.invocationDetails.relativeFile;
-        }
-        const payloadToSend = {
-          "saveResults": shouldScanTestForAccessibility,
-          "testDetails": {
-            "name": attributes.title,
-            "testRunId": '5058', // variable not consumed, shouldn't matter what we send
-            "filePath": filePath,
-            "scopeList": [
-              filePath,
-              attributes.title
-            ]
-          },
-          "platform": {
-            "os_name": os_data,
-            "os_version": Cypress.env("OS_VERSION"),
-            "browser_name": Cypress.browser.name,
-            "browser_version": Cypress.browser.version
-          }
-        };
-        browserStackLog(`Saving accessibility test results`);
-        cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}).then(() => {
-          browserStackLog(`Saved accessibility test results`);
-        })
+  cy.task('readFileMaybe', 'testDetails.json').then(data => {
+    browserStackLog('FILE CONTENT ::::: ' + data)
+    if (data === null) return;
 
-      } catch (er) {
-      }
+    let testDetails = {}
+    try {
+      testDetails = JSON.parse(data);
+    } catch (err) {
+      browserStackLog('Error while parsing json for testDetails:' + err)
+    }
+    const testId = testDetails[attributes.fullTitle()]
+    browserStackLog('TestId : ' + testId)
+    cy.window().then(async (win) => {
+      let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
+      sendToReporter({[attributes.title]: shouldScanTestForAccessibility})
+      if (!shouldScanTestForAccessibility) return cy.wrap({});
+
+      cy.wrap(performScan(win), {timeout: 30000}).then(() => {
+        try {
+          let os_data;
+          if (Cypress.env("OS")) {
+            os_data = Cypress.env("OS");
+          } else {
+            os_data = Cypress.platform === 'linux' ? 'mac' : "win"
+          }
+          let filePath = '';
+          if (attributes.invocationDetails !== undefined && attributes.invocationDetails.relativeFile !== undefined) {
+            filePath = attributes.invocationDetails.relativeFile;
+          }
+          const payloadToSend = {
+            'thTestRunUuid': testId,
+            'thBuildUuid': Cypress.env("BROWSERSTACK_TESTHUB_UUID"),
+            'thJwtToken': Cypress.env('BROWSERSTACK_TESTHUB_JWT')
+          };
+          browserStackLog(`Saving accessibility test results`);
+          cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}).then(() => {
+            browserStackLog(`Saved accessibility test results`);
+          })
+
+        } catch (er) {
+        }
+      })
     })
   });
 })
diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js
index dd47f208..60f75219 100644
--- a/bin/accessibility-automation/plugin/index.js
+++ b/bin/accessibility-automation/plugin/index.js
@@ -1,7 +1,13 @@
 
 const path = require("node:path");
+const fs = require('node:fs');
+const ipc = require('node-ipc');
+const { connectIPCClient } = require('../../testObservability/plugin/ipcClient');
+const { IPC_EVENTS } = require('../../testObservability/helper/constants');
 
 const browserstackAccessibility = (on, config) => {
+  connectIPCClient(config);
+
   let browser_validation = true;
   if (process.env.BROWSERSTACK_ACCESSIBILITY_DEBUG === 'true') {
     config.env.BROWSERSTACK_LOGS = 'true';
@@ -13,6 +19,19 @@ const browserstackAccessibility = (on, config) => {
 
       return null
     },
+
+    test_accessibility_data(data) {
+      ipc.of.browserstackTestObservability.emit(IPC_EVENTS.ACCESSIBILITY_DATA, data);
+      return null;
+    },
+
+    readFileMaybe(filename) {
+      if (fs.existsSync(filename)) {
+        return fs.readFileSync(filename, 'utf8')
+      }
+
+      return null
+    }
   })
   on('before:browser:launch', (browser = {}, launchOptions) => {
     try {
@@ -41,6 +60,8 @@ const browserstackAccessibility = (on, config) => {
   config.env.ACCESSIBILITY_EXTENSION_PATH = process.env.ACCESSIBILITY_EXTENSION_PATH
   config.env.OS_VERSION = process.env.OS_VERSION
   config.env.OS = process.env.OS
+  config.env.BROWSERSTACK_TESTHUB_UUID = process.env.BROWSERSTACK_TESTHUB_UUID
+  config.env.BROWSERSTACK_TESTHUB_JWT = process.env.BROWSERSTACK_TESTHUB_JWT
 
   config.env.IS_ACCESSIBILITY_EXTENSION_LOADED = browser_validation.toString()
 
diff --git a/bin/commands/runs.js b/bin/commands/runs.js
index 79282b73..6a41c679 100644
--- a/bin/commands/runs.js
+++ b/bin/commands/runs.js
@@ -39,6 +39,8 @@ const {
   supportFileCleanup
 } = require('../accessibility-automation/helper');
 const { isTurboScaleSession, getTurboScaleGridDetails, patchCypressConfigFileContent, atsFileCleanup } = require('../helpers/atsHelper');
+const TestHubHandler = require('../testhub/testhubHandler');
+const { shouldProcessEventForTesthub, checkAndSetAccessibility } = require('../testhub/utils');
 
 module.exports = function run(args, rawArgs) {
 
@@ -75,6 +77,7 @@ module.exports = function run(args, rawArgs) {
     const turboScaleSession = isTurboScaleSession(bsConfig);
     Constants.turboScaleObj.enabled = turboScaleSession;
 
+    checkAndSetAccessibility(bsConfig);
     utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);
 
     utils.setDefaults(bsConfig, args);
@@ -114,10 +117,11 @@ module.exports = function run(args, rawArgs) {
     utils.setBuildTags(bsConfig, args);
 
     /* 
-      Send build start to Observability
+      Send build start to testHub
     */
-    if(isTestObservabilitySession) {
-      await launchTestSession(bsConfig, bsConfigPath);
+    if(shouldProcessEventForTesthub()) {
+      await TestHubHandler.launchBuild(bsConfig, bsConfigPath);
+      // await launchTestSession(bsConfig, bsConfigPath);
       utils.setO11yProcessHooks(null, bsConfig, args, null, buildReportData);
     }
     
@@ -149,9 +153,6 @@ module.exports = function run(args, rawArgs) {
       // add cypress dependency if missing
       utils.setCypressNpmDependency(bsConfig);
 
-      if (isAccessibilitySession && isBrowserstackInfra) {
-        await createAccessibilityTestRun(bsConfig);
-      }
 
       if (turboScaleSession) {
         // Local is only required in case user is running on trial grid and wants to access private website.
diff --git a/bin/helpers/helper.js b/bin/helpers/helper.js
index de5e7825..52b38e26 100644
--- a/bin/helpers/helper.js
+++ b/bin/helpers/helper.js
@@ -172,6 +172,17 @@ exports.getGitMetaData = () => {
     }
   })
 }
+
+exports.getHostInfo = () => {
+  return {
+    hostname: os.hostname(),
+    platform: os.platform(),
+    type: os.type(),
+    version: os.version(),
+    arch: os.arch()
+  }
+}
+
 exports.getCiInfo = () => {
   var env = process.env;
   // Jenkins
@@ -309,7 +320,7 @@ exports.setBrowserstackCypressCliDependency = (bsConfig) => {
     typeof runSettings.npm_dependencies === 'object') {
     if (!("browserstack-cypress-cli" in runSettings.npm_dependencies)) {
       logger.warn("Missing browserstack-cypress-cli not found in npm_dependencies");        
-      runSettings.npm_dependencies['browserstack-cypress-cli'] = this.getAgentVersion() || "latest";
+      runSettings.npm_dependencies['browserstack-cypress-cli'] = "/Users/saurav/Home/Browserstack/Automate/cypress_cli_main/browserstack-cypress-cli/browserstack-cypress-cli-1.29.1.tgz";
       logger.warn(`Adding browserstack-cypress-cli version ${runSettings.npm_dependencies['browserstack-cypress-cli']} in npm_dependencies`);
     }
   }
diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js
index a56ea54d..2f64cd8b 100644
--- a/bin/helpers/utils.js
+++ b/bin/helpers/utils.js
@@ -26,6 +26,7 @@ const usageReporting = require("./usageReporting"),
   { OBSERVABILITY_ENV_VARS, TEST_OBSERVABILITY_REPORTER } = require('../testObservability/helper/constants');
 
 const request = require('request');
+const { shouldProcessEventForTesthub } = require("../testhub/utils");
 
 exports.validateBstackJson = (bsConfigPath) => {
   return new Promise(function (resolve, reject) {
@@ -1350,7 +1351,7 @@ exports.setEnforceSettingsConfig = (bsConfig) => {
 
 // blindly send other passed configs with run_settings and handle at backend
 exports.setOtherConfigs = (bsConfig, args) => {
-  if(o11yHelpers.isTestObservabilitySession() && process.env.BS_TESTOPS_JWT) {
+  if(shouldProcessEventForTesthub()) {
     bsConfig["run_settings"]["reporter"] = TEST_OBSERVABILITY_REPORTER;
     return;
   }
diff --git a/bin/testObservability/helper/constants.js b/bin/testObservability/helper/constants.js
index 63bf08d1..336fe3e1 100644
--- a/bin/testObservability/helper/constants.js
+++ b/bin/testObservability/helper/constants.js
@@ -11,7 +11,8 @@ exports.IPC_EVENTS = {
   SCREENSHOT: 'testObservability:cypressScreenshot',
   COMMAND: 'testObservability:cypressCommand',
   CUCUMBER: 'testObservability:cypressCucumberStep',
-  PLATFORM_DETAILS: 'testObservability:cypressPlatformDetails'
+  PLATFORM_DETAILS: 'testObservability:cypressPlatformDetails',
+  ACCESSIBILITY_DATA: 'accessibility:cypressAccessibilityData'
 };
 
 exports.OBSERVABILITY_ENV_VARS = [
diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js
index deacff95..062c949c 100644
--- a/bin/testObservability/helper/helper.js
+++ b/bin/testObservability/helper/helper.js
@@ -107,7 +107,7 @@ exports.printBuildLink = async (shouldStopSession, exitCode = null) => {
   if(exitCode) process.exit(exitCode);
 }
 
-const nodeRequest = (type, url, data, config) => {
+exports.nodeRequest = (type, url, data, config) => {
   return new Promise(async (resolve, reject) => {
     const options = {...config,...{
       method: type,
@@ -242,7 +242,7 @@ exports.getPackageVersion = (package_, bsConfig = null) => {
   return packageVersion;
 }
 
-const setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUILD_HASHED_ID, BS_TESTOPS_ALLOW_SCREENSHOTS, OBSERVABILITY_LAUNCH_SDK_VERSION) => {
+exports.setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUILD_HASHED_ID, BS_TESTOPS_ALLOW_SCREENSHOTS, OBSERVABILITY_LAUNCH_SDK_VERSION) => {
   process.env.BS_TESTOPS_JWT = BS_TESTOPS_JWT;
   process.env.BS_TESTOPS_BUILD_HASHED_ID = BS_TESTOPS_BUILD_HASHED_ID;
   process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = BS_TESTOPS_ALLOW_SCREENSHOTS;
@@ -316,7 +316,7 @@ exports.setCrashReportingConfigFromReporter = (credentialsStr, bsConfigPath, cyp
   }
 }
 
-const setCrashReportingConfig = (bsConfig, bsConfigPath) => {
+exports.setCrashReportingConfig = (bsConfig, bsConfigPath) => {
   try {
     const browserstackConfigFile = utils.readBsConfigJSON(bsConfigPath);
     const cypressConfigFile = getCypressConfigFileContent(bsConfig, null);
@@ -334,7 +334,7 @@ const setCrashReportingConfig = (bsConfig, bsConfigPath) => {
 }
 
 exports.launchTestSession = async (user_config, bsConfigPath) => {
-  setCrashReportingConfig(user_config, bsConfigPath);
+  exports.setCrashReportingConfig(user_config, bsConfigPath);
   
   const obsUserName = user_config["auth"]["username"];
   const obsAccessKey = user_config["auth"]["access_key"];
@@ -387,10 +387,10 @@ exports.launchTestSession = async (user_config, bsConfigPath) => {
         }
       };
 
-      const response = await nodeRequest('POST','api/v1/builds',data,config);
+      const response = await exports.nodeRequest('POST','api/v1/builds',data,config);
       exports.debug('Build creation successfull!');
       process.env.BS_TESTOPS_BUILD_COMPLETED = true;
-      setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion);
+      exports.setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion);
       if(this.isBrowserstackInfra()) helper.setBrowserstackCypressCliDependency(user_config);
     } catch(error) {
       if(!error.errorType) {
@@ -417,7 +417,7 @@ exports.launchTestSession = async (user_config, bsConfigPath) => {
       }
 
       process.env.BS_TESTOPS_BUILD_COMPLETED = false;
-      setEnvironmentVariablesForRemoteReporter(null, null, null);
+      exports.setEnvironmentVariablesForRemoteReporter(null, null, null);
     }
   }
 }
@@ -474,7 +474,7 @@ exports.batchAndPostEvents = async (eventUrl, kind, data) => {
   };
 
   try {
-    const response = await nodeRequest('POST',eventUrl,data,config);
+    const response = await exports.nodeRequest('POST',eventUrl,data,config);
     if(response.data.error) {
       throw({message: response.data.error});
     } else {
@@ -491,6 +491,18 @@ exports.batchAndPostEvents = async (eventUrl, kind, data) => {
   }
 }
 
+const shouldUploadEvent = (eventType) => {
+  isAccessibility = utils.isTrueString(process.env.BROWSERSTACK_TEST_ACCESSIBILITY) || !utils.isUndefined(process.env.ACCESSIBILITY_AUTH);
+  if(!this.isTestObservabilitySession() || isAccessibility) {
+    if (['HookRunStarted', 'HookRunFinished', 'LogCreated', 'BuildUpdate'].includes(eventType)) {
+      return false;
+    }
+    return true;
+  }
+
+  return this.isTestObservabilitySession() || isAccessibility;
+}
+
 const RequestQueueHandler = require('./requestQueueHandler');
 exports.requestQueueHandler = new RequestQueueHandler();
 
@@ -506,9 +518,9 @@ exports.uploadEventData = async (eventData, run=0) => {
     ['BuildUpdate']: 'Build_Update'
   }[eventData.event_type];
 
+  if (!shouldUploadEvent(eventData.event_type)) return;
   if(run === 0 && process.env.BS_TESTOPS_JWT != "null") exports.pending_test_uploads.count += 1;
-  
-  if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true") {
+  if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true" || process.env.ACCESSIBILITY_AUTH) {
     if(process.env.BS_TESTOPS_JWT == "null") {
       exports.debug(`EXCEPTION IN ${log_tag} REQUEST TO TEST OBSERVABILITY : missing authentication token`);
       exports.pending_test_uploads.count = Math.max(0,exports.pending_test_uploads.count-1);
@@ -537,7 +549,7 @@ exports.uploadEventData = async (eventData, run=0) => {
       };
   
       try {
-        const response = await nodeRequest('POST',event_api_url,data,config);
+        const response = await exports.nodeRequest('POST',event_api_url,data,config);
         if(response.data.error) {
           throw({message: response.data.error});
         } else {
@@ -626,7 +638,7 @@ exports.shouldReRunObservabilityTests = () => {
 }
 
 exports.stopBuildUpstream = async () => {
-  if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true") {
+  if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true" || process.env.ACCESSIBILITY_AUTH) {
     if(process.env.BS_TESTOPS_JWT == "null" || process.env.BS_TESTOPS_BUILD_HASHED_ID == "null") {
       exports.debug('EXCEPTION IN stopBuildUpstream REQUEST TO TEST OBSERVABILITY : Missing authentication token');
       return {
@@ -646,7 +658,7 @@ exports.stopBuildUpstream = async () => {
       };
   
       try {
-        const response = await nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config);
+        const response = await exports.nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config);
         if(response.data && response.data.error) {
           throw({message: response.data.error});
         } else {
diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js
index 33c515f6..25c52101 100644
--- a/bin/testObservability/reporter/index.js
+++ b/bin/testObservability/reporter/index.js
@@ -61,6 +61,7 @@ const {
 } = require('../helper/helper');
 
 const { consoleHolder } = require('../helper/constants');
+const { shouldProcessEventForTesthub, appendTestHubParams } = require('../../testhub/utils');
 
 // this reporter outputs test results, indenting two spaces per suite
 class MyReporter {
@@ -76,6 +77,7 @@ class MyReporter {
     this.platformDetailsMap = {};
     this.runStatusMarkedHash = {};
     this.haveSentBuildUpdate = false;
+    this.accessibilityScanInfo = {};
     this.registerListeners();
     setCrashReportingConfigFromReporter(null, process.env.OBS_CRASH_REPORTING_BS_CONFIG_PATH, process.env.OBS_CRASH_REPORTING_CYPRESS_CONFIG_PATH);
 
@@ -87,7 +89,7 @@ class MyReporter {
       })
 
       .on(EVENT_HOOK_BEGIN, async (hook) => {
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           if(!hook.hookAnalyticsId) {
             hook.hookAnalyticsId = uuidv4();
           } else if(this.runStatusMarkedHash[hook.hookAnalyticsId]) {
@@ -102,7 +104,7 @@ class MyReporter {
       })
 
       .on(EVENT_HOOK_END, async (hook) => {
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           if(!this.runStatusMarkedHash[hook.hookAnalyticsId]) {
             if(!hook.hookAnalyticsId) {
               /* Hook objects don't maintain uuids in Cypress-Mocha */
@@ -123,7 +125,7 @@ class MyReporter {
       })
 
       .on(EVENT_TEST_PASS, async (test) => {
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           if(!this.runStatusMarkedHash[test.testAnalyticsId]) {
             if(test.testAnalyticsId) this.runStatusMarkedHash[test.testAnalyticsId] = true;
             await this.sendTestRunEvent(test);
@@ -132,7 +134,7 @@ class MyReporter {
       })
 
       .on(EVENT_TEST_FAIL, async (test, err) => {
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           if((test.testAnalyticsId && !this.runStatusMarkedHash[test.testAnalyticsId]) || (test.hookAnalyticsId && !this.runStatusMarkedHash[test.hookAnalyticsId])) {
             if(test.testAnalyticsId) {
               this.runStatusMarkedHash[test.testAnalyticsId] = true;
@@ -146,7 +148,7 @@ class MyReporter {
       })
 
       .on(EVENT_TEST_PENDING, async (test) => {
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           if(!test.testAnalyticsId) test.testAnalyticsId = uuidv4();
           if(!this.runStatusMarkedHash[test.testAnalyticsId]) {
             this.runStatusMarkedHash[test.testAnalyticsId] = true;
@@ -157,14 +159,14 @@ class MyReporter {
 
       .on(EVENT_TEST_BEGIN, async (test) => {
         if (this.runStatusMarkedHash[test.testAnalyticsId]) return;
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           await this.testStarted(test);
         }
       })
 
       .on(EVENT_TEST_END, async (test) => {
         if (this.runStatusMarkedHash[test.testAnalyticsId]) return;
-        if(this.testObservability == true) {
+        if(shouldProcessEventForTesthub()) {
           if(!this.runStatusMarkedHash[test.testAnalyticsId]) {
             if(test.testAnalyticsId) this.runStatusMarkedHash[test.testAnalyticsId] = true;
             await this.sendTestRunEvent(test);
@@ -174,7 +176,7 @@ class MyReporter {
       
       .once(EVENT_RUN_END, async () => {
         try {
-          if(this.testObservability == true) {
+          if(shouldProcessEventForTesthub()) {
             const hookSkippedTests = getHookSkippedTests(this.runner.suite);
             for(const test of hookSkippedTests) {
               if(!test.testAnalyticsId) test.testAnalyticsId = uuidv4();
@@ -199,6 +201,7 @@ class MyReporter {
         server.on(IPC_EVENTS.COMMAND, this.cypressCommandListener.bind(this));
         server.on(IPC_EVENTS.CUCUMBER, this.cypressCucumberStepListener.bind(this));
         server.on(IPC_EVENTS.PLATFORM_DETAILS, this.cypressPlatformDetailsListener.bind(this));
+        server.on(IPC_EVENTS.ACCESSIBILITY_DATA, this.cypressAccessibilityDataListener.bind(this));
       },
       (server) => {
         server.off(IPC_EVENTS.CONFIG, '*');
@@ -319,6 +322,9 @@ class MyReporter {
         }
       };
 
+      this.persistTestId(testData, eventType)
+      appendTestHubParams(testData, eventType, this.accessibilityScanInfo)
+
       if(eventType.match(/TestRunFinished/) || eventType.match(/TestRunSkipped/)) {
         testData['meta'].steps = JSON.parse(JSON.stringify(this.currentTestCucumberSteps));
         this.currentTestCucumberSteps = [];
@@ -560,6 +566,25 @@ class MyReporter {
     this.currentCypressVersion = cypressVersion;
   }
 
+  cypressAccessibilityDataListener = async(accessibilityData) => {
+    this.accessibilityScanInfo = {...this.accessibilityScanInfo, ...accessibilityData}
+  }
+
+  persistTestId = (testData, eventType) => {
+    if (!eventType.match(/TestRun/)) {return}
+
+    const fileName = 'testDetails.json'
+    let testDetails = {};
+    try {
+      if(fs.existsSync(fileName)) {
+        testDetails = JSON.parse(fs.readFileSync(fileName).toString())
+      }
+      testDetails[testData.identifier] = testData.uuid
+      fs.writeFileSync(fileName, JSON.stringify(testDetails))
+      consoleHolder.log('FILE WRITTEN ::::::::: ' + JSON.stringify(testDetails))
+    } catch (err) {}
+  }
+
   getFormattedArgs = (args) => {
     if(!args) return '';
     let res = '';
diff --git a/bin/testhub/constants.js b/bin/testhub/constants.js
new file mode 100644
index 00000000..6be9945f
--- /dev/null
+++ b/bin/testhub/constants.js
@@ -0,0 +1,10 @@
+module.exports = {
+  'TESTHUB_BUILD_API': 'api/v2/builds',
+  'ACCESSIBILITY': 'accessibility',
+  'OBSERVABILITY': 'observability',
+  'ERROR': {
+    'INVALID_CREDENTIALS': 'ERROR_INVALID_CREDENTIALS',
+    'DEPRECATED': 'ERROR_SDK_DEPRECATED',
+    'ACCESS_DENIED': 'ERROR_ACCESS_DENIED'
+  },
+};
diff --git a/bin/testhub/plugin/index.js b/bin/testhub/plugin/index.js
new file mode 100644
index 00000000..f146b647
--- /dev/null
+++ b/bin/testhub/plugin/index.js
@@ -0,0 +1,9 @@
+const accessibilityPlugin = require('../../accessibility-automation/plugin/index')
+const testObservabilityPlugin = require('../../testObservability/plugin')
+
+const browserstackPlugin = (on, config) => {
+  accessibilityPlugin(on, config)
+  testObservabilityPlugin(on, config)
+}
+
+module.exports = browserstackPlugin
diff --git a/bin/testhub/testhubHandler.js b/bin/testhub/testhubHandler.js
new file mode 100644
index 00000000..cecf9fab
--- /dev/null
+++ b/bin/testhub/testhubHandler.js
@@ -0,0 +1,119 @@
+const logger = require('../../bin/helpers/logger').winstonLogger;
+const { setCrashReportingConfig, nodeRequest, isTestObservabilitySession } = require("../testObservability/helper/helper");
+const helper = require('../helpers/helper');
+const testhubUtils = require('./utils');
+const TESTHUB_CONSTANTS = require('./constants');
+
+class TestHubHandler {
+  static async launchBuild(user_config, bsConfigPath) {
+    setCrashReportingConfig(user_config, bsConfigPath);
+  
+    const obsUserName = user_config["auth"]["username"];
+    const obsAccessKey = user_config["auth"]["access_key"];
+    
+    const BSTestOpsToken = `${obsUserName || ''}:${obsAccessKey || ''}`;
+    if(BSTestOpsToken === '') {
+      // if olly true 
+      if (isTestObservabilitySession()) {
+        logger.debug('EXCEPTION IN BUILD START EVENT : Missing authentication token');
+        process.env.BS_TESTOPS_BUILD_COMPLETED = false;
+      }
+
+      if (testhubUtils.isAccessibilityEnabled()) {
+        logger.debug('Exception while creating test run for BrowserStack Accessibility Automation: Missing authentication token');
+        process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'false'
+      }
+
+      return [null, null];
+    }
+
+    try {
+      const data = await this.generateBuildUpstreamData(user_config);
+      const config = this.getConfig(obsUserName, obsAccessKey);
+      const response = await nodeRequest('POST', TESTHUB_CONSTANTS.TESTHUB_BUILD_API, data, config);
+      const launchData = this.extractDataFromResponse(user_config, data, response, config);
+    } catch (error) {
+      if (error.success === false) { // non 200 response
+        testhubUtils.logBuildError(error);
+        return;
+      }
+    }
+  }
+
+  static async extractDataFromResponse(user_config, requestData, response, config) {
+    const launchData = {};
+
+    if (isTestObservabilitySession()) {
+      const [jwt, buildHashedId, allowScreenshot] = testhubUtils.setTestObservabilityVariables(user_config, requestData, response.data);
+      if (jwt && buildHashedId) {
+        launchData[TESTHUB_CONSTANTS.OBSERVABILITY] =  {jwt, buildHashedId, allowScreenshot};
+        process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'true';
+      } else {
+        launchData[TESTHUB_CONSTANTS.OBSERVABILITY] = {};
+        process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false';
+      }
+    } else {
+      process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false';
+    }
+
+    if(testhubUtils.isAccessibilityEnabled()) {
+      const [authToken, buildHashedId] = testhubUtils.setAccessibilityVariables(user_config, response.data);
+      if (authToken && buildHashedId) {
+        launchData[TESTHUB_CONSTANTS.ACCESSIBILITY] = {authToken, buildHashedId};
+        process.env.BROWSERSTACK_ACCESSIBILITY = 'true';
+        testhubUtils.checkAndSetAccessibility(user_config, true);
+      } else {
+        launchData[TESTHUB_CONSTANTS.ACCESSIBILITY] = {};
+        process.env.BROWSERSTACK_ACCESSIBILITY = 'false';
+        testhubUtils.checkAndSetAccessibility(user_config, false);
+      }
+    } else {
+      process.env.BROWSERSTACK_ACCESSIBILITY = 'false';
+      testhubUtils.checkAndSetAccessibility(user_config, false)
+    }
+
+    if (testhubUtils.shouldProcessEventForTesthub()) {
+      testhubUtils.setTestHubCommonMetaInfo(user_config, response.data);
+    }
+  }
+
+  static async generateBuildUpstreamData(user_config) {
+    const {buildName, projectName, buildDescription, buildTags} = helper.getBuildDetails(user_config, true);
+    const productMap = testhubUtils.getProductMap(user_config);
+    const data = {
+      'project_name': projectName,
+      'name': buildName,
+      'build_identifier': '', // no build identifier in cypress
+      'description': buildDescription || '',
+      'started_at': (new Date()).toISOString(),
+      'tags': buildTags,
+      'host_info': helper.getHostInfo(),
+      'ci_info': helper.getCiInfo(),
+      'build_run_identifier': process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER,
+      'failed_tests_rerun': process.env.BROWSERSTACK_RERUN || false,
+      'version_control': await helper.getGitMetaData(),
+      'accessibility': testhubUtils.getAccessibilityOptions(user_config),
+      'framework_details': testhubUtils.getFrameworkDetails(),
+      'product_map': productMap,
+      'browserstackAutomation': productMap['automate']
+    };
+
+    return data;
+  }
+
+  static getConfig(obsUserName, obsAccessKey) {
+    return {
+      auth: {
+        username: obsUserName,
+        password: obsAccessKey
+      },
+      headers: {
+        'Content-Type': 'application/json',
+        'X-BSTACK-TESTOPS': 'true'
+      }
+    };
+  }
+}
+
+
+module.exports = TestHubHandler;
diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js
new file mode 100644
index 00000000..f7a8f917
--- /dev/null
+++ b/bin/testhub/utils.js
@@ -0,0 +1,205 @@
+const os = require('os');
+
+const logger = require('../../bin/helpers/logger').winstonLogger;
+const TESTHUB_CONSTANTS = require('./constants');
+const testObservabilityHelper = require('../../bin/testObservability/helper/helper');
+const helper = require('../helpers/helper');
+const accessibilityHelper = require('../accessibility-automation/helper');
+
+const isUndefined = value => (value === undefined || value === null);
+
+exports.getFrameworkDetails = (user_config) => {
+  return {
+    'frameworkName': 'Cypress',
+    'frameworkVersion': testObservabilityHelper.getPackageVersion('cypress', user_config),
+    'sdkVersion': helper.getAgentVersion(),
+    'language': 'javascript',
+    'testFramework': {
+      'name': 'cypress',
+      'version': helper.getPackageVersion('cypress', user_config)
+    }
+  };
+};
+
+exports.isAccessibilityEnabled = () => {
+  if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY !== undefined) {
+    return process.env.BROWSERSTACK_TEST_ACCESSIBILITY === 'true';
+  }
+  return false;
+}
+
+// app-automate and percy support is not present for cypress
+exports.getProductMap = (user_config) => {
+  return {
+    'observability': testObservabilityHelper.isTestObservabilitySession(),
+    'accessibility': exports.isAccessibilityEnabled(user_config),
+    'percy': false,
+    'automate': testObservabilityHelper.isBrowserstackInfra(),
+    'app_automate': false
+  };
+};
+
+exports.shouldProcessEventForTesthub = () => {
+  return testObservabilityHelper.isTestObservabilitySession() || exports.isAccessibilityEnabled();
+}
+
+exports.setTestObservabilityVariables = (user_config, requestData, responseData) => {
+  if (!responseData.observability) {
+    exports.handleErrorForObservability();
+
+    return [null, null, null];
+  }
+
+  if (!responseData.observability.success) {
+    exports.handleErrorForObservability(responseData.observability);
+
+    return [null, null, null];
+  }
+
+  if (testObservabilityHelper.isBrowserstackInfra()) {
+    process.env.BS_TESTOPS_BUILD_COMPLETED = true;
+    testObservabilityHelper.setEnvironmentVariablesForRemoteReporter(responseData.jwt, responseData.build_hashed_id, responseData.observability.options.allow_screenshots.toString(), requestData.framework_details.sdkVersion);
+    helper.setBrowserstackCypressCliDependency(user_config)
+    return [responseData.jwt, responseData.build_hashed_id, process.env.BS_TESTOPS_ALLOW_SCREENSHOTS];
+  }
+  return [null, null, null];
+}
+
+exports.handleErrorForObservability = (error) => {
+  process.env.BROWSERSTACK_TESTHUB_UUID = 'null';
+  process.env.BROWSERSTACK_TESTHUB_JWT = 'null';
+  process.env.BS_TESTOPS_BUILD_COMPLETED = 'false';
+  process.env.BS_TESTOPS_JWT = 'null';
+  process.env.BS_TESTOPS_BUILD_HASHED_ID = 'null';
+  process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = 'null';
+  exports.logBuildError(error, TESTHUB_CONSTANTS.OBSERVABILITY);
+};
+
+exports.setAccessibilityVariables = (user_config, responseData) => {
+  if (!responseData.accessibility) {
+    exports.handleErrorForAccessibility(user_config);
+
+    return [null, null];
+  }
+
+  if (!responseData.accessibility.success) {
+    exports.handleErrorForAccessibility(user_config, responseData.accessibility);
+
+    return [null, null];
+  }
+
+  if(responseData.accessibility.options) {
+    logger.debug(`BrowserStack Accessibility Automation Build Hashed ID: ${responseData.build_hashed_id}`);
+    setAccessibilityCypressCapabilities(user_config, responseData);
+    helper.setBrowserstackCypressCliDependency(user_config);
+    return [process.env.ACCESSIBILITY_AUTH, responseData.build_hashed_id];
+  }
+  return [null, null];
+}
+
+const setAccessibilityCypressCapabilities = (user_config, responseData) => {
+  if (isUndefined(user_config.run_settings.accessibilityOptions)) {
+    user_config.run_settings.accessibilityOptions = {}
+  }
+  const {accessibilityToken, scannerVersion} = jsonifyAccessibilityArray(responseData.accessibility.options.capabilities, 'name', 'value');
+  process.env.ACCESSIBILITY_AUTH = accessibilityToken
+  process.env.ACCESSIBILITY_SCANNERVERSION = scannerVersion
+  
+  user_config.run_settings.accessibilityOptions["authToken"] = accessibilityToken;
+  user_config.run_settings.accessibilityOptions["auth"] = accessibilityToken;
+  user_config.run_settings.accessibilityOptions["scannerVersion"] = scannerVersion;
+  user_config.run_settings.system_env_vars.push('ACCESSIBILITY_AUTH')
+  user_config.run_settings.system_env_vars.push('ACCESSIBILITY_SCANNERVERSION')
+  this.checkAndSetAccessibility(user_config, true)
+}
+
+// To handle array of json, eg: [{keyName : '', valueName : ''}]
+const jsonifyAccessibilityArray = (dataArray, keyName, valueName) => {
+  const result = {};
+  dataArray.forEach(element => {
+    result[element[keyName]] = element[valueName];
+  });
+
+  return result;
+};
+
+
+exports.handleErrorForAccessibility = (user_config, error) => {
+  this.checkAndSetAccessibility(user_config, false)
+  process.env.BROWSERSTACK_TESTHUB_UUID = 'null';
+  process.env.BROWSERSTACK_TESTHUB_JWT = 'null';
+  exports.logBuildError(error, TESTHUB_CONSTANTS.ACCESSIBILITY);
+};
+
+exports.logBuildError = (error, product = '') => {
+  if (error === undefined) {
+    logger.error(`${product.toUpperCase()} Build creation failed`);
+
+    return;
+  }
+
+  try {
+    for (const errorJson of error.errors) {
+      const errorType = errorJson.key;
+      const errorMessage = errorJson.message;
+      if (errorMessage) {
+        switch (errorType) {
+          case TESTHUB_CONSTANTS.ERROR.INVALID_CREDENTIALS:
+            logger.error(errorMessage);
+            break;
+          case TESTHUB_CONSTANTS.ERROR.ACCESS_DENIED:
+            logger.info(errorMessage);
+            break;
+          case TESTHUB_CONSTANTS.ERROR.DEPRECATED:
+            logger.error(errorMessage);
+            break;
+          default:
+            logger.error(errorMessage);
+        }
+      }
+    }
+  } catch (e) {
+    logger.error(error)
+  }
+};
+
+exports.setTestHubCommonMetaInfo = (user_config, responseData) => {
+  process.env.BROWSERSTACK_TESTHUB_JWT = responseData.jwt;
+  process.env.BROWSERSTACK_TESTHUB_UUID = responseData.build_hashed_id;
+  user_config.run_settings.system_env_vars.push('BROWSERSTACK_TESTHUB_JWT')
+  user_config.run_settings.system_env_vars.push('BROWSERSTACK_TESTHUB_UUID')
+};
+
+exports.checkAndSetAccessibility = (user_config, accessibilityFlag) => {
+  if (!user_config.run_settings.system_env_vars.includes('BROWSERSTACK_TEST_ACCESSIBILITY')) {
+    user_config.run_settings.system_env_vars.push('BROWSERSTACK_TEST_ACCESSIBILITY')
+  }
+
+  // if flag already provided, then set the value and return
+  if (!isUndefined(accessibilityFlag)) {
+    process.env.BROWSERSTACK_TEST_ACCESSIBILITY = accessibilityFlag.toString();
+    user_config.run_settings.accessibility = accessibilityFlag;
+    return;
+  }
+
+  if (!accessibilityHelper.isAccessibilitySupportedCypressVersion(user_config.run_settings.cypress_config_file) ){
+    logger.warn(`Accessibility Testing is not supported on Cypress version 9 and below.`)
+    process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'false';
+    user_config.run_settings.accessibility = false;
+    return;
+  }
+
+  isAccessibilityTestEnabled = (user_config.run_settings.accessibility || accessibilityHelper.checkAccessibilityPlatform(user_config)) && testObservabilityHelper.isBrowserstackInfra();
+  process.env.BROWSERSTACK_TEST_ACCESSIBILITY = isAccessibilityTestEnabled.toString();
+}
+
+exports.getAccessibilityOptions = (user_config) => {
+  const settings = isUndefined(user_config.run_settings.accessibilityOptions) ? {} : user_config.run_settings.accessibilityOptions
+  return {'settings': settings};
+}
+
+exports.appendTestHubParams = (testData, eventType, accessibilityScanInfo) => {
+  if (exports.isAccessibilityEnabled() && !['HookRunStarted', 'HookRunFinished', 'TestRunStarted'].includes(eventType) && !isUndefined(accessibilityScanInfo[testData.name])) {
+    testData['product_map'] = {'accessibility' : accessibilityScanInfo[testData.name]}
+  }
+}