import * as FunnelTestEvent from '../../src/instrumentation/funnelInstrumentation.js'
import { sendFinish, sendStart } from '../../src/instrumentation/funnelInstrumentation.js'
import { BStackLogger } from '../../src/bstackLogger.js'
import fs from 'node:fs'
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import { FUNNEL_INSTRUMENTATION_URL } from '../../src/constants.js'
import type { BrowserstackHealing } from '@browserstack/ai-sdk-node'
vi.mock('fetch')
const mockedFetch = vi.mocked(fetch)
const config = {
userName: 'your-username',
accessKey: 'your-access-key',
testObservability: { enabled: true },
framework: 'framework',
buildName: 'build-name',
buildIdentifier: 'your-build-identifier',
accessibility: true,
percy: true,
automate: true,
appAutomate: false,
}
const expectedEventData = {
userName: '[REDACTED]',
accessKey: '[REDACTED]',
event_type: 'SDKTestAttempted',
detectedFramework: 'WebdriverIO-framework',
event_properties: {
language_framework: 'WebdriverIO_framework',
referrer: expect.stringContaining('WebdriverIO-'),
language: 'WebdriverIO',
languageVersion: process.version,
buildName: config.buildName,
buildIdentifier: config.buildIdentifier,
os: expect.any(String),
hostname: expect.any(String),
productMap: {
'observability': true,
'accessibility': true,
'percy': true,
'automate': true,
'app_automate': false
},
product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']),
framework: 'framework'
}
}
describe('funnelInstrumentation', () => {
let originalCwd: { (): string; (): string }
beforeEach(() => {
originalCwd = process.cwd
process.cwd = () => '/path/to/project'
vi.spyOn(BStackLogger, 'warn').mockImplementation(() => {})
})
afterEach(() => {
process.cwd = originalCwd
vi.restoreAllMocks()
vi.resetAllMocks()
vi.clearAllMocks()
})
describe('sendStart', () => {
it('does nothing if userName or accessKey is missing in config', async () => {
const config = { userName: '', accessKey: '' }
await FunnelTestEvent.sendStart(config as any)
expect(fetch).not.toHaveBeenCalled()
})
it('sendStart calls sends request with correct data', async () => {
await sendStart(config as any)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: expect.any(Object),
body: expect.any(String)
}))
})
})
describe('sendFinish', () => {
it('sendFinish calls sends request with correct data', async () => {
const finishConfig = {
...config,
'accessibility': false,
'percy': false,
}
const finishExpectedEventData = {
...expectedEventData,
event_type: 'SDKTestSuccessful',
event_properties: {
...expectedEventData.event_properties,
productMap: {
'observability': true,
'accessibility': false,
'percy': false,
'automate': true,
'app_automate': false
},
product: expect.arrayContaining(['observability', 'automate']),
productUsage: expect.objectContaining({
testObservability: expect.any(Object)
})
},
}
await sendFinish(finishConfig as any)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: expect.any(Object),
body: expect.any(String)
}))
})
})
it('saveFunnelData writes data to file and returns file path', () => {
BStackLogger.ensureLogsFolder = vi.fn()
vi.spyOn(fs, 'writeFileSync').mockImplementationOnce(() => {})
const filePath = FunnelTestEvent.saveFunnelData('SDKTestSuccessful', config as any)
expect(fs.writeFileSync).toHaveBeenCalledWith(filePath, expect.any(String))
})
it('fireFunnelRequest sends request with correct data', async () => {
const data = { key: 'value', userName: '[REDACTED]', accessKey: '[REDACTED]' }
mockedFetch.mockReturnValueOnce(Promise.resolve(Response.json({})))
await FunnelTestEvent.fireFunnelRequest(data)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: expect.any(Object),
body: JSON.stringify(data)
}))
})
describe('Healing instrumentation', () => {
it('should not send instrumentation event in case user receives an upgrade required warning', async () => {
const authResult = { message: 'Upgrade required' } as BrowserstackHealing.InitErrorResponse
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
expect(fetch).not.toHaveBeenCalled()
})
it('should send server error event when isAuthenticated is false and status is 5xx', async () => {
const authResult = { isAuthenticated: false, status: 500 } as BrowserstackHealing.InitErrorResponse
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
'content-type': 'application/json',
},
body: expect.stringContaining('SDKTestTcgDownResponse')
}))
const [[, { body }]] = (fetch as jest.Mock).mock.calls
const parsedBody = JSON.parse(body)
expectedEventData.event_type = 'SDKTestTcgDownResponse'
expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
})
it('should send initialization success event when userId is present', async () => {
const authResult = {
isAuthenticated: true,
userId: 123456,
groupId: 234567,
sessionToken: 'session-token',
isGroupAIEnabled: true,
defaultLogDataEnabled: true,
isHealingEnabled: true
} as BrowserstackHealing.InitSuccessResponse
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
'content-type': 'application/json',
},
body: expect.stringContaining('SDKTestTcgtInitSuccessful')
}))
const [[, { body }]] = (fetch as jest.Mock).mock.calls
const parsedBody = JSON.parse(body)
expectedEventData.event_type = 'SDKTestTcgtInitSuccessful'
expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
})
it('should send initialization failed event if status is 4xx', async () => {
const authResult = {
isAuthenticated: true,
status: 400,
message: 'Request failed with status code 400'
} as any
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
'content-type': 'application/json',
},
body: expect.stringContaining('SDKTestInitFailedResponse')
}))
const [[, { body }]] = (fetch as jest.Mock).mock.calls
const parsedBody = JSON.parse(body)
expectedEventData.event_type = 'SDKTestInitFailedResponse'
expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
})
it('should send server error event when isAuthenticated is false and status is 5xx', async () => {
const authResult = { isAuthenticated: true } as any
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
'content-type': 'application/json',
},
body: expect.stringContaining('SDKTestInvalidTcgAuthResponseWithUserImpact')
}))
const [[, { body }]] = (fetch as jest.Mock).mock.calls
const parsedBody = JSON.parse(body)
expectedEventData.event_type = 'SDKTestInvalidTcgAuthResponseWithUserImpact'
expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
})
it('should not send user impact event if selfHeal is not enabled by user', async () => {
const authResult = {
isAuthenticated: true
} as any
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, false)
expect(fetch).not.toHaveBeenCalled()
})
it('should handle exceptions during healing instrumentation', async () => {
const authResult = {
isAuthenticated: true,
get userId() {
throw new Error('Property access error')
}
} as any
const debugSpy = vi.spyOn(BStackLogger, 'debug')
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('Error in handling healing instrumentation:'))
expect(fetch).not.toHaveBeenCalled()
})
it('should handle healing not enabled for user', async () => {
const authResult = {
isAuthenticated: true,
userId: 123456,
groupId: 234567,
sessionToken: 'session-token',
isGroupAIEnabled: true,
defaultLogDataEnabled: true,
isHealingEnabled: false
} as BrowserstackHealing.InitSuccessResponse
FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, false)
expect(fetch).not.toHaveBeenCalled()
})
it('should handle healing disabled for the group', async () => {
const authResult = {
isAuthenticated: true,
userId: 123456,
groupId: 234567,
sessionToken: 'session-token',
isGroupAIEnabled: true,
defaultLogDataEnabled: true,
isHealingEnabled: false
} as BrowserstackHealing.InitSuccessResponse
const bstackConfig = {
...config,
selfHeal: true
}
FunnelTestEvent.handleHealingInstrumentation(authResult, bstackConfig as any, true)
expect(fetch).not.toHaveBeenCalled()
expect(BStackLogger.warn).toHaveBeenCalledWith('Healing is not enabled for your group, please contact the admin')
})
})
})