From 8f87ee64a62fbfbecfec17d42df4010cbb3ad124 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 20:35:23 +0530 Subject: [PATCH 1/3] fix(observability): report Mocha/Jasmine test tags Port of the main-line fix to the v8 branch; the reported build pinned --wdio_versions v8. WDIO Mocha and Jasmine never populated a test-level tags field, so every Observability test arrived with tags == []. @tag tokens in the suite and test titles are the source, matching the node SDK's Jest/Playwright convention. The leading @ is kept so these match Cucumber pickle tags. Both event paths fixed: wdioMochaTestFramework#getTestData now sets KEY_TEST_TAGS (declared in the service's constants, never written, long consumed by the binary), and insights-handler#getRunData now sets tags on TestData. Inert for untagged suites -- a title with no @ token yields []. Co-Authored-By: Claude Opus 5 (1M context) --- .../cli/frameworks/wdioMochaTestFramework.ts | 7 ++-- .../src/insights-handler.ts | 6 +++- packages/browserstack-service/src/util.ts | 21 ++++++++++++ .../browserstack-service/tests/util.test.ts | 34 +++++++++++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts b/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts index 4c384b1f..b5706ee2 100644 --- a/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts +++ b/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts @@ -10,7 +10,7 @@ import TrackedInstance from '../instances/trackedInstance.js' import { TestFrameworkConstants } from './constants/testFrameworkConstants.js' import { BStackLogger as logger } from '../cliLogger.js' import type { Frameworks } from '@wdio/types' -import { getGitMetaData, getMochaTestHierarchy, getUniqueIdentifier, isUndefined, removeAnsiColors } from '../../util.js' +import { getGitMetaData, getMochaTestHierarchy, getTestTags, getUniqueIdentifier, isUndefined, removeAnsiColors } from '../../util.js' import { TEST_ANALYTICS_ID } from '../../constants.js' export default class WdioMochaTestFramework extends TestFramework { @@ -192,6 +192,8 @@ export default class WdioMochaTestFramework extends TestFramework { const gitConfig = await getGitMetaData() const filename = test.file // || this._suiteFile + const scopes = getMochaTestHierarchy(test) + const testData: Record = { [TestFrameworkConstants.KEY_TEST_ID]: getUniqueIdentifier(test, framework), [TestFrameworkConstants.KEY_TEST_NAME]: test.title || test.description, @@ -199,7 +201,8 @@ export default class WdioMochaTestFramework extends TestFramework { [TestFrameworkConstants.KEY_TEST_FILE_PATH]: (gitConfig?.root && filename) ? path.relative(gitConfig.root, filename) : undefined, [TestFrameworkConstants.KEY_TEST_LOCATION]: filename ? path.relative(process.cwd(), filename) : undefined, [TestFrameworkConstants.KEY_TEST_SCOPE]: fullTitle, - [TestFrameworkConstants.KEY_TEST_SCOPES]: getMochaTestHierarchy(test), + [TestFrameworkConstants.KEY_TEST_SCOPES]: scopes, + [TestFrameworkConstants.KEY_TEST_TAGS]: getTestTags(test, scopes), } return testData diff --git a/packages/browserstack-service/src/insights-handler.ts b/packages/browserstack-service/src/insights-handler.ts index 767b4d8e..ee2a23ae 100644 --- a/packages/browserstack-service/src/insights-handler.ts +++ b/packages/browserstack-service/src/insights-handler.ts @@ -15,6 +15,7 @@ import { getGitMetaData, getHookType, getPlatformVersion, getScenarioExamples, + getTestTags, getUniqueIdentifier, getUniqueIdentifierForCucumber, isBrowserstackSession, @@ -725,6 +726,8 @@ class _InsightsHandler { InsightsHandler.currentTest.name = test.title || test.description } + const scopes = this.getHierarchy(test) + const testData: TestData = { uuid: testMetaData.uuid, type: test.type || 'test', @@ -734,7 +737,8 @@ class _InsightsHandler { code: test.body }, scope: fullTitle, - scopes: this.getHierarchy(test), + scopes, + tags: getTestTags(test, scopes), identifier: fullTitle, file_name: filename ? path.relative(process.cwd(), filename) : undefined, location: filename ? path.relative(process.cwd(), filename) : undefined, diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 8a14f3be..fc516f89 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1937,6 +1937,27 @@ export function getMochaTestHierarchy(test: Frameworks.Test) { return value.reverse() } +const TEST_TAG_PATTERN = /@[\w-]+/g + +/** + * Mocha and Jasmine have no tag construct, so `@tag` tokens written into the suite and + * test titles are the tag source — the same convention the node SDK uses for Jest and + * Playwright. The leading `@` is kept so these match the Cucumber runner's pickle tags, + * which reach Observability with it intact. + */ +export function getTestTags(test: Frameworks.Test, scopes?: string[]): string[] { + const titles = [...(scopes ?? getMochaTestHierarchy(test)), test.title || test.description || ''] + const tags: string[] = [] + for (const title of titles) { + for (const tag of title.match(TEST_TAG_PATTERN) || []) { + if (!tags.includes(tag)) { + tags.push(tag) + } + } + } + return tags +} + /** * Checks if the capabilities represent a multiremote configuration * @param capabilities - The capabilities to check diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index 5414eab0..139ff0c9 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -2282,3 +2282,37 @@ describe('getCentralUser', () => { expect(utils.getCentralUser()).toEqual({}) }) }) + +describe('getTestTags', () => { + const tagsFor = (title: string, scopes: string[] = []) => + utils.getTestTags({ title } as any, scopes) + + it('picks up a tag in the test title', () => { + expect(tagsFor('logs in @smoke')).toEqual(['@smoke']) + }) + + it('picks up a tag from the describe scope', () => { + expect(tagsFor('logs in', ['auth @regression'])).toEqual(['@regression']) + }) + + it('merges scope and title tags, deduped', () => { + expect(tagsFor('logs in @smoke', ['auth @smoke', 'nested @regression'])) + .toEqual(['@smoke', '@regression']) + }) + + it('returns an empty array when nothing is tagged', () => { + expect(tagsFor('logs in', ['auth'])).toEqual([]) + }) + + it('picks up multiple tags from one title', () => { + expect(tagsFor('logs in @smoke @p1')).toEqual(['@smoke', '@p1']) + }) + + it('keeps hyphens in a tag', () => { + expect(tagsFor('logs in @smoke-test')).toEqual(['@smoke-test']) + }) + + it('falls back to the Jasmine description when there is no title', () => { + expect(utils.getTestTags({ description: 'logs in @jasmine' } as any, [])).toEqual(['@jasmine']) + }) +}) From 5dcf7b82e9ee63d49d9cfaa72ce21a4ab4aedaf2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:06:06 +0000 Subject: [PATCH 2/3] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-215.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-215.md diff --git a/.changeset/pr-215.md b/.changeset/pr-215.md new file mode 100644 index 00000000..b7406483 --- /dev/null +++ b/.changeset/pr-215.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed test tags not being reported to Test Observability for Mocha and Jasmine. Tags written as `@tag` tokens in suite or test titles are now sent with each test. From efd87cfdac639361b1237fd00f757910fe50fee6 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Wed, 23 Sep 2026 11:18:27 +0530 Subject: [PATCH 3/3] fix(observability): address review on Mocha/Jasmine test tags Port of the main-line review fixes (#214). - Tag pattern now requires the @ to start a token. /@[\w-]+/g matched any embedded @, so 'user@example.com' produced a bogus @example tag. - reporter.ts#getRunData now sets tags. insights-handler#getRunData is reached only for mocha, so jasmine events and mocha skipped tests went through the reporter, which set scopes but no tags -- the Jasmine claim was false without this. - Doc comment no longer claims node-agent parity; that SDK strips the @. - Tests: embedded-@ cases, plus one per hierarchy shape with scopes omitted, pinned to the order the helper actually returns. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/reporter.ts | 4 +++- packages/browserstack-service/src/util.ts | 10 ++++++---- .../browserstack-service/tests/util.test.ts | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/browserstack-service/src/reporter.ts b/packages/browserstack-service/src/reporter.ts index 88608e03..d9449827 100644 --- a/packages/browserstack-service/src/reporter.ts +++ b/packages/browserstack-service/src/reporter.ts @@ -17,7 +17,8 @@ import { getGitMetaData, removeAnsiColors, getHookType, - getPlatformVersion + getPlatformVersion, + getTestTags } from './util.js' import { BStackLogger } from './bstackLogger.js' import type { Capabilities } from '@wdio/types' @@ -270,6 +271,7 @@ class _TestReporter extends WDIOReporter { }, scope: scope, scopes: scopes, + tags: getTestTags(testStats as unknown as Frameworks.Test, scopes), identifier: identifier, file_name: suiteFileName ? path.relative(process.cwd(), suiteFileName) : undefined, location: suiteFileName ? path.relative(process.cwd(), suiteFileName) : undefined, diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index fc516f89..5e7c3a24 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1937,13 +1937,15 @@ export function getMochaTestHierarchy(test: Frameworks.Test) { return value.reverse() } -const TEST_TAG_PATTERN = /@[\w-]+/g +// The lookbehind is load-bearing: without it every `@` starts a match, so an address +// like `user@example.com` in a title yields a bogus `@example` tag. +const TEST_TAG_PATTERN = /(? { it('falls back to the Jasmine description when there is no title', () => { expect(utils.getTestTags({ description: 'logs in @jasmine' } as any, [])).toEqual(['@jasmine']) }) + + it('ignores an @ embedded in a larger token', () => { + expect(tagsFor('sends the invite to user@example.com')).toEqual([]) + expect(tagsFor('installs pkg@1.2.3')).toEqual([]) + }) + + it('derives scopes from the mocha hierarchy when none are supplied', () => { + const test = { + title: 'logs in @smoke', + ctx: { test: {} }, + parent: { title: 'auth @regression', parent: { title: '' } } + } + expect(utils.getTestTags(test as any)).toEqual(['@regression', '@smoke']) + }) + + it('derives scopes from the jasmine hierarchy when none are supplied', () => { + const test = { description: 'logs in @smoke', fullName: 'auth @regression logs in @smoke' } + expect(utils.getTestTags(test as any)).toEqual(['@regression', '@smoke']) + }) })