diff --git a/eslint.config.mjs b/eslint.config.mjs index ca936c44..879d8e50 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,4 @@ import { defineConfig } from 'eslint/config'; -import globals from 'globals'; import tseslint from 'typescript-eslint'; import pluginReact from 'eslint-plugin-react'; import importPlugin from 'eslint-plugin-import'; @@ -48,7 +47,8 @@ export default defineConfig([ maxLength: 90, ignoreUrls: true, }, - ] - } + ], + 'import/no-cycle': "error" + }, }, -]); \ No newline at end of file +]); diff --git a/src/api-intercept.spec.ts b/src/api-intercept.spec.ts new file mode 100644 index 00000000..84575377 --- /dev/null +++ b/src/api-intercept.spec.ts @@ -0,0 +1,856 @@ +import * as apiIntercept from './api-intercept'; +import * as config from './config'; +import * as embedConfig from './embed/embedConfig'; +import { InterceptedApiType, EmbedEvent, BaseViewConfig } from './types'; +import { embedEventStatus } from './utils'; +import { logger } from './utils/logger'; + +jest.mock('./config'); +jest.mock('./embed/embedConfig'); +jest.mock('./utils/logger'); + +const mockGetThoughtSpotHost = config.getThoughtSpotHost as jest.Mock; +const mockGetEmbedConfig = embedConfig.getEmbedConfig as jest.Mock; +const mockLogger = logger as jest.Mocked; + +describe('api-intercept', () => { + const thoughtSpotHost = 'https://test.thoughtspot.com'; + let originalJsonParse: any; + + beforeAll(() => { + originalJsonParse = JSON.parse; + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockGetThoughtSpotHost.mockReturnValue(thoughtSpotHost); + mockGetEmbedConfig.mockReturnValue({}); + // Restore JSON.parse before each test + JSON.parse = originalJsonParse; + }); + + afterEach(() => { + // Ensure JSON.parse is restored after each test + JSON.parse = originalJsonParse; + }); + + describe('getInterceptInitData', () => { + it('should return default intercept flags when no intercepts are configured', () => { + const viewConfig: BaseViewConfig = {}; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result).toEqual({ + interceptUrls: [], + interceptTimeout: undefined, + }); + }); + + it('should expand InterceptedApiType.AnswerData to specific URLs', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [InterceptedApiType.AnswerData] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toEqual([ + `${thoughtSpotHost}/prism/?op=GetChartWithData`, + `${thoughtSpotHost}/prism/?op=GetTableWithHeadlineData`, + `${thoughtSpotHost}/prism/?op=GetTableWithData`, + ]); + }); + + it('should expand InterceptedApiType.LiveboardData to specific URLs', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [InterceptedApiType.LiveboardData] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toEqual([ + `${thoughtSpotHost}/prism/?op=LoadContextBook` + ]); + }); + + it('should handle multiple intercept types', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [ + InterceptedApiType.AnswerData, + InterceptedApiType.LiveboardData + ] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toContain(`${thoughtSpotHost}/prism/?op=GetChartWithData`); + expect(result.interceptUrls).toContain(`${thoughtSpotHost}/prism/?op=LoadContextBook`); + expect(result.interceptUrls.length).toBe(4); + }); + + it('should handle custom URL strings', () => { + const customUrl = '/api/custom-endpoint'; + const viewConfig: BaseViewConfig = { + interceptUrls: [customUrl] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toEqual([`${thoughtSpotHost}${customUrl}`]); + }); + + it('should handle full URL strings', () => { + const fullUrl = 'https://example.com/api/endpoint'; + const viewConfig: BaseViewConfig = { + interceptUrls: [fullUrl] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toEqual([fullUrl]); + }); + + it('should handle InterceptedApiType.ALL', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [InterceptedApiType.ALL] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toEqual([InterceptedApiType.ALL]); + }); + + it('should prioritize ALL over other intercept types', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [ + InterceptedApiType.AnswerData, + InterceptedApiType.ALL, + '/api/custom' + ] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toEqual([InterceptedApiType.ALL]); + }); + + it('should handle legacy isOnBeforeGetVizDataInterceptEnabled flag', () => { + const viewConfig: any = { + isOnBeforeGetVizDataInterceptEnabled: true + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toContain(`${thoughtSpotHost}/prism/?op=GetChartWithData`); + }); + + it('should combine legacy flag with interceptUrls', () => { + const viewConfig: any = { + isOnBeforeGetVizDataInterceptEnabled: true, + interceptUrls: [InterceptedApiType.LiveboardData] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls).toContain(`${thoughtSpotHost}/prism/?op=GetChartWithData`); + expect(result.interceptUrls).toContain(`${thoughtSpotHost}/prism/?op=LoadContextBook`); + }); + + it('should pass through interceptTimeout', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [], + interceptTimeout: 5000 + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptTimeout).toBe(5000); + }); + + it('should deduplicate URLs when same type is added multiple times', () => { + const viewConfig: BaseViewConfig = { + interceptUrls: [ + InterceptedApiType.AnswerData, + InterceptedApiType.AnswerData + ] + }; + + const result = apiIntercept.getInterceptInitData(viewConfig); + + expect(result.interceptUrls.length).toBe(3); // 3 answer data URLs + }); + }); + + describe('handleInterceptEvent', () => { + let executeEvent: jest.Mock; + let getUnsavedAnswerTml: jest.Mock; + let viewConfig: BaseViewConfig; + + beforeEach(() => { + executeEvent = jest.fn(); + getUnsavedAnswerTml = jest.fn().mockResolvedValue({ answer: { tml: 'test-tml' } }); + viewConfig = {}; + }); + + it('should handle valid intercept data', async () => { + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: { + method: 'POST', + body: JSON.stringify({ + variables: { + session: { sessionId: 'session-123' }, + contextBookId: 'viz-456' + } + }) + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.objectContaining({ + input: '/prism/?op=GetChartWithData', + urlType: InterceptedApiType.AnswerData + }) + ); + }); + + it('should trigger legacy OnBeforeGetVizDataIntercept for answer data URLs', async () => { + viewConfig.isOnBeforeGetVizDataInterceptEnabled = true; + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: { + method: 'POST', + body: JSON.stringify({ + variables: { + session: { sessionId: 'session-123' }, + contextBookId: 'viz-456' + } + }) + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(getUnsavedAnswerTml).toHaveBeenCalledWith({ + sessionId: 'session-123', + vizId: 'viz-456' + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.OnBeforeGetVizDataIntercept, + { + data: { + data: { answer: { tml: 'test-tml' } }, + status: embedEventStatus.END, + type: EmbedEvent.OnBeforeGetVizDataIntercept + } + } + ); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.any(Object) + ); + }); + + it('should not trigger legacy intercept for non-answer data URLs', async () => { + viewConfig.isOnBeforeGetVizDataInterceptEnabled = true; + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=LoadContextBook', + init: { + method: 'POST', + body: '{}' + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(getUnsavedAnswerTml).not.toHaveBeenCalled(); + expect(executeEvent).toHaveBeenCalledTimes(1); + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.any(Object) + ); + }); + + it('should handle GetTableWithHeadlineData URL as answer data', async () => { + viewConfig.isOnBeforeGetVizDataInterceptEnabled = true; + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetTableWithHeadlineData', + init: { + body: JSON.stringify({ + variables: { session: { sessionId: 'test' } } + }) + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(getUnsavedAnswerTml).toHaveBeenCalled(); + }); + + it('should handle GetTableWithData URL as answer data', async () => { + viewConfig.isOnBeforeGetVizDataInterceptEnabled = true; + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetTableWithData', + init: { + body: JSON.stringify({ + variables: { session: { sessionId: 'test' } } + }) + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(getUnsavedAnswerTml).toHaveBeenCalled(); + }); + + it('should handle invalid JSON in event data', async () => { + const eventData = { + data: 'invalid-json' + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.Error, + { error: 'Error parsing api intercept body' } + ); + expect(mockLogger.error).toHaveBeenCalled(); + }); + + it('should handle init with non-JSON body', async () => { + const eventData = { + data: JSON.stringify({ + input: '/api/test', + init: { + method: 'POST', + body: 'plain-text-body' + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.objectContaining({ + init: expect.objectContaining({ + body: 'plain-text-body' + }) + }) + ); + }); + + it('should handle malformed event data structure with property access error', async () => { + // Create an object with a getter that throws when accessing 'input' + global.JSON.parse = jest.fn().mockImplementationOnce((str) => { + // Return an object with a getter that throws + return new Proxy({}, { + get(target, prop) { + if (prop === 'input') { + throw new Error('Property access error'); + } + return undefined; + } + }); + }); + + const eventData = { + data: JSON.stringify({ input: '/test', init: {} }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.Error, + { error: 'Error parsing api intercept body' } + ); + expect(mockLogger.error).toHaveBeenCalled(); + + // Explicitly restore for this test + global.JSON.parse = originalJsonParse; + }); + + it('should determine urlType as ALL for unknown URLs', async () => { + const eventData = { + data: JSON.stringify({ + input: '/unknown/endpoint', + init: { method: 'GET' } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.objectContaining({ + urlType: InterceptedApiType.ALL + }) + ); + }); + + it('should determine urlType as LiveboardData for liveboard URLs', async () => { + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=LoadContextBook', + init: { method: 'POST' } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.objectContaining({ + urlType: InterceptedApiType.LiveboardData + }) + ); + }); + + it('should handle event data with missing init', async () => { + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData' + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + // When init is missing, accessing init.body throws an error + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.Error, + { error: 'Error parsing api intercept body' } + ); + }); + + it('should handle event data with missing body', async () => { + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.any(Object) + ); + }); + + it('should handle event data with missing variables in body', async () => { + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: { + body: JSON.stringify({}) + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.any(Object) + ); + }); + + it('should handle event data with missing session in variables', async () => { + const eventData = { + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: { + body: JSON.stringify({ + variables: {} + }) + } + }) + }; + + await apiIntercept.handleInterceptEvent({ + eventData, + executeEvent, + viewConfig, + getUnsavedAnswerTml + }); + + expect(executeEvent).toHaveBeenCalledWith( + EmbedEvent.ApiIntercept, + expect.any(Object) + ); + }); + }); + + describe('processApiInterceptResponse', () => { + it('should process legacy format with error', () => { + const legacyPayload = { + data: { + error: { + errorText: 'Test Error', + errorDescription: 'Test Description' + }, + execute: false + } + }; + + const result = apiIntercept.processApiInterceptResponse(legacyPayload); + + expect(result).toEqual({ + data: { + execute: false, + response: { + body: { + errors: [ + { + title: 'Test Error', + description: 'Test Description', + isUserError: true, + }, + ], + data: {}, + }, + }, + } + }); + }); + + it('should pass through new format unchanged', () => { + const newPayload = { + execute: true, + response: { + body: { data: 'test' } + } + }; + + const result = apiIntercept.processApiInterceptResponse(newPayload); + + expect(result).toEqual(newPayload); + }); + + it('should handle payload without data property', () => { + const payload = { + execute: true + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle payload with data but no error', () => { + const payload = { + data: { + execute: true, + someOtherProperty: 'value' + } + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle null payload', () => { + const result = apiIntercept.processApiInterceptResponse(null); + + expect(result).toBeNull(); + }); + + it('should handle undefined payload', () => { + const result = apiIntercept.processApiInterceptResponse(undefined); + + expect(result).toBeUndefined(); + }); + + it('should handle payload with null data', () => { + const payload: any = { + data: null + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle payload with data.error set to null', () => { + const payload: any = { + data: { + error: null, + execute: true + } + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle payload with data.error set to undefined', () => { + const payload: any = { + data: { + error: undefined, + execute: true + } + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle payload with data.error set to false', () => { + const payload = { + data: { + error: false, + execute: true + } + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle payload with data.error set to 0', () => { + const payload = { + data: { + error: 0, + execute: true + } + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + + it('should handle payload with data.error set to empty string', () => { + const payload = { + data: { + error: '', + execute: true + } + }; + + const result = apiIntercept.processApiInterceptResponse(payload); + + expect(result).toEqual(payload); + }); + }); + + describe('processLegacyInterceptResponse', () => { + it('should convert legacy error format to new format', () => { + const legacyPayload = { + data: { + error: { + errorText: 'Custom Error', + errorDescription: 'Custom Description' + }, + execute: false + } + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result).toEqual({ + data: { + execute: false, + response: { + body: { + errors: [ + { + title: 'Custom Error', + description: 'Custom Description', + isUserError: true, + }, + ], + data: {}, + }, + }, + } + }); + }); + + it('should handle missing error properties', () => { + const legacyPayload = { + data: { + error: {}, + execute: true + } + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.response.body.errors[0]).toEqual({ + title: undefined, + description: undefined, + isUserError: true, + }); + }); + + it('should handle missing execute property', () => { + const legacyPayload = { + data: { + error: { + errorText: 'Error', + errorDescription: 'Description' + } + } + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.execute).toBeUndefined(); + }); + + it('should always include empty data object', () => { + const legacyPayload = { + data: { + error: { + errorText: 'Error', + errorDescription: 'Description' + }, + execute: false + } + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.response.body.data).toEqual({}); + }); + + it('should always set isUserError to true', () => { + const legacyPayload = { + data: { + error: { + errorText: 'Error', + errorDescription: 'Description' + }, + execute: false + } + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.response.body.errors[0].isUserError).toBe(true); + }); + + it('should handle payload with null data', () => { + const legacyPayload: any = { + data: null + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.execute).toBeUndefined(); + expect(result.data.response.body.errors[0].title).toBeUndefined(); + expect(result.data.response.body.errors[0].description).toBeUndefined(); + }); + + it('should handle payload with null error', () => { + const legacyPayload: any = { + data: { + error: null, + execute: true + } + }; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.execute).toBe(true); + expect(result.data.response.body.errors[0].title).toBeUndefined(); + expect(result.data.response.body.errors[0].description).toBeUndefined(); + }); + + it('should handle payload with undefined properties', () => { + const legacyPayload = {}; + + const result = apiIntercept.processLegacyInterceptResponse(legacyPayload); + + expect(result.data.execute).toBeUndefined(); + expect(result.data.response.body.errors[0].title).toBeUndefined(); + expect(result.data.response.body.errors[0].description).toBeUndefined(); + }); + }); +}); + diff --git a/src/api-intercept.ts b/src/api-intercept.ts new file mode 100644 index 00000000..824594d8 --- /dev/null +++ b/src/api-intercept.ts @@ -0,0 +1,204 @@ +import { getThoughtSpotHost } from "./config"; +import { getEmbedConfig } from "./embed/embedConfig"; +import { InterceptedApiType, BaseViewConfig, ApiInterceptFlags, EmbedEvent } from "./types"; +import { embedEventStatus } from "./utils"; +import { logger } from "./utils/logger"; + +const DefaultInterceptUrlsMap: Record, string[]> = { + [InterceptedApiType.AnswerData]: [ + '/prism/?op=GetChartWithData', + '/prism/?op=GetTableWithHeadlineData', + '/prism/?op=GetTableWithData', + ] as string[], + [InterceptedApiType.LiveboardData]: [ + '/prism/?op=LoadContextBook' + ] as string[], +}; + +const formatInterceptUrl = (url: string) => { + const host = getThoughtSpotHost(getEmbedConfig()); + if (url.startsWith('/')) return `${host}${url}`; + return url; +} + +interface LegacyInterceptFlags { + isOnBeforeGetVizDataInterceptEnabled: boolean; +} +/** + * Converts user passed url values to proper urls + * [ANSER_DATA] => ['https://host/pris/op?=op'] + * @param interceptUrls + * @returns + */ +const processInterceptUrls = (interceptUrls: (string | InterceptedApiType)[]) => { + let processedUrls = [...interceptUrls]; + Object.entries(DefaultInterceptUrlsMap).forEach(([apiType, apiTypeUrls]) => { + if (!processedUrls.includes(apiType)) return; + processedUrls = processedUrls.filter(url => url !== apiType); + processedUrls = [...processedUrls, ...apiTypeUrls]; + }) + return processedUrls.map(url => formatInterceptUrl(url)); +} + +/** + * Returns the data to be sent to embed to setup intercepts + * the urls to intercept, timeout etc + * @param viewConfig + * @returns + */ +export const getInterceptInitData = (viewConfig: BaseViewConfig): Required> => { + const combinedUrls = [...(viewConfig.interceptUrls || [])]; + + if ((viewConfig as LegacyInterceptFlags).isOnBeforeGetVizDataInterceptEnabled) { + combinedUrls.push(InterceptedApiType.AnswerData); + } + + const shouldInterceptAll = combinedUrls.includes(InterceptedApiType.ALL); + const interceptUrls = shouldInterceptAll ? [InterceptedApiType.ALL] : processInterceptUrls(combinedUrls); + + const interceptTimeout = viewConfig.interceptTimeout; + + return { + interceptUrls, + interceptTimeout, + }; +} + +const parseJson = (jsonString: string): [any, Error | null] => { + try { + const json = JSON.parse(jsonString); + return [json, null]; + } catch (error) { + return [null, error]; + } +} + +/** + * Parse the api intercept data and return the parsed data and error if any + * Embed returns the input and init from the fetch call + */ +const parseInterceptData = (eventDataString: any) => { + + try { + const [parsedData, error] = parseJson(eventDataString); + if (error) { + return [null, error]; + } + + const { input, init } = parsedData; + + const [parsedBody, bodyParseError] = parseJson(init.body); + if (!bodyParseError) { + init.body = parsedBody; + } + + const parsedInit = { input, init }; + return [parsedInit, null]; + } catch (error) { + return [null, error]; + } +} + +const getUrlType = (url: string) => { + for (const [apiType, apiTypeUrls] of Object.entries(DefaultInterceptUrlsMap)) { + if (apiTypeUrls.includes(url)) return apiType as InterceptedApiType; + } + // TODO: have a unknown type maybe ?? + return InterceptedApiType.ALL; +} + +/** + * Handle Api intercept event and simulate legacy onBeforeGetVizDataIntercept event + * + * embed sends -> ApiIntercept -> we send + * ApiIntercept + * OnBeforeGetVizDataIntercept (if url is part of DefaultUrlMap.AnswerData) + * + * @param params + * @returns + */ +export const handleInterceptEvent = async (params: { + eventData: any, + executeEvent: (eventType: EmbedEvent, data: any) => void, + viewConfig: BaseViewConfig, + getUnsavedAnswerTml: (props: { sessionId?: string, vizId?: string }) => Promise<{ tml: string }> +}) => { + + const { eventData, executeEvent, viewConfig, getUnsavedAnswerTml } = params; + + const [interceptData, bodyParseError] = parseInterceptData(eventData.data); + + if (bodyParseError) { + executeEvent(EmbedEvent.Error, { + error: 'Error parsing api intercept body', + }); + logger.error('Error parsing request body', bodyParseError); + return; + } + + + const { input: requestUrl, init } = interceptData; + + const sessionId = init?.body?.variables?.session?.sessionId; + const vizId = init?.body?.variables?.contextBookId; + + const answerDataUrls = DefaultInterceptUrlsMap[InterceptedApiType.AnswerData]; + const legacyInterceptEnabled = viewConfig.isOnBeforeGetVizDataInterceptEnabled; + const isAnswerDataUrl = answerDataUrls.includes(requestUrl); + const sendLegacyIntercept = isAnswerDataUrl && legacyInterceptEnabled; + if (sendLegacyIntercept) { + const answerTml = await getUnsavedAnswerTml({ sessionId, vizId }); + // Build the legacy payload for backwards compatibility + const legacyPayload = { + data: { + data: answerTml, + status: embedEventStatus.END, + type: EmbedEvent.OnBeforeGetVizDataIntercept + } + } + executeEvent(EmbedEvent.OnBeforeGetVizDataIntercept, legacyPayload); + } + + const urlType = getUrlType(requestUrl); + executeEvent(EmbedEvent.ApiIntercept, { ...interceptData, urlType }); +} + +/** + * Support both the legacy and new format of the api intercept response + * @param payload + * @returns + */ +export const processApiInterceptResponse = (payload: any) => { + const isLegacyFormat = payload?.data?.error; + + if (isLegacyFormat) { + return processLegacyInterceptResponse(payload); + } + + return payload; +} + +export const processLegacyInterceptResponse = (payload: any) => { + + const errorText = payload?.data?.error?.errorText; + const errorDescription = payload?.data?.error?.errorDescription; + + const payloadToSend = { + execute: payload?.data?.execute, + response: { + body: { + errors: [ + { + title: errorText, + description: errorDescription, + isUserError: true, + }, + ], + data: {}, + }, + }, + }; + + + return { data: payloadToSend }; +} diff --git a/src/embed/app.ts b/src/embed/app.ts index 6adecdd1..189f0835 100644 --- a/src/embed/app.ts +++ b/src/embed/app.ts @@ -512,11 +512,6 @@ export interface AppViewConfig extends AllEmbedViewConfig { * ``` */ dataPanelCustomGroupsAccordionInitialState?: DataPanelCustomColumnGroupsAccordionState; - /** - * Flag that allows using `EmbedEvent.OnBeforeGetVizDataIntercept`. - * @version SDK : 1.29.0 | ThoughtSpot: 10.1.0.cl - */ - isOnBeforeGetVizDataInterceptEnabled?: boolean; /** * Flag to use home page search bar mode * @@ -668,8 +663,6 @@ export class AppEmbed extends V1Embed { collapseSearchBarInitially = false, enable2ColumnLayout, enableCustomColumnGroups = false, - isOnBeforeGetVizDataInterceptEnabled = false, - dataPanelCustomGroupsAccordionInitialState = DataPanelCustomColumnGroupsAccordionState.EXPAND_ALL, collapseSearchBar = true, isLiveboardCompactHeaderEnabled = false, @@ -752,13 +745,6 @@ export class AppEmbed extends V1Embed { params[Param.enableAskSage] = enableAskSage; } - if (isOnBeforeGetVizDataInterceptEnabled) { - - params[ - Param.IsOnBeforeGetVizDataInterceptEnabled - ] = isOnBeforeGetVizDataInterceptEnabled; - } - if (homePageSearchBarMode) { params[Param.HomePageSearchBarMode] = homePageSearchBarMode; } diff --git a/src/embed/hostEventClient/contracts.ts b/src/embed/hostEventClient/contracts.ts index f82077b7..0f2e07f1 100644 --- a/src/embed/hostEventClient/contracts.ts +++ b/src/embed/hostEventClient/contracts.ts @@ -7,6 +7,7 @@ export enum UIPassthroughEvent { GetAvailableUIPassthroughs = 'getAvailableUiPassthroughs', GetAnswerConfig = 'getAnswerPageConfig', GetLiveboardConfig = 'getPinboardPageConfig', + GetUnsavedAnswerTML = 'getUnsavedAnswerTML', } // UI Passthrough Contract @@ -63,6 +64,15 @@ export type UIPassthroughContractBase = { request: any; response: any; }; + [UIPassthroughEvent.GetUnsavedAnswerTML]: { + request: { + sessionId?: string; + vizId?: string; + }; + response: { + tml: string; + }; + }; }; // UI Passthrough Request and Response diff --git a/src/embed/search.ts b/src/embed/search.ts index 534db212..e572b9ac 100644 --- a/src/embed/search.ts +++ b/src/embed/search.ts @@ -26,6 +26,7 @@ import { ERROR_MESSAGE } from '../errors'; import { getAuthPromise } from './base'; import { getReleaseVersion } from '../auth'; import { getEmbedConfig } from './embedConfig'; +import { getInterceptInitData } from '../api-intercept'; /** * Configuration for search options. @@ -276,13 +277,6 @@ export interface SearchViewConfig * @deprecated Use {@link collapseSearchBar} instead */ collapseSearchBarInitially?: boolean; - /** - * Flag to enable onBeforeSearchExecute Embed Event - * - * Supported embed types: `SearchEmbed` - * @version: SDK: 1.29.0 | ThoughtSpot: 10.1.0.cl - */ - isOnBeforeGetVizDataInterceptEnabled?: boolean; /** * This controls the initial behaviour of custom column groups accordion. * It takes DataPanelCustomColumnGroupsAccordionState enum values as input. @@ -397,8 +391,6 @@ export class SearchEmbed extends TsEmbed { runtimeParameters, collapseSearchBarInitially = false, enableCustomColumnGroups = false, - isOnBeforeGetVizDataInterceptEnabled = false, - dataPanelCustomGroupsAccordionInitialState = DataPanelCustomColumnGroupsAccordionState.EXPAND_ALL, focusSearchBarOnRender = true, excludeRuntimeParametersfromURL, @@ -442,11 +434,6 @@ export class SearchEmbed extends TsEmbed { queryParams[Param.HideSearchBar] = true; } - if (isOnBeforeGetVizDataInterceptEnabled) { - - queryParams[Param.IsOnBeforeGetVizDataInterceptEnabled] = isOnBeforeGetVizDataInterceptEnabled; - } - if (!focusSearchBarOnRender) { queryParams[Param.FocusSearchBarOnRender] = focusSearchBarOnRender; } diff --git a/src/embed/ts-embed.spec.ts b/src/embed/ts-embed.spec.ts index 9e1ef649..f676c566 100644 --- a/src/embed/ts-embed.spec.ts +++ b/src/embed/ts-embed.spec.ts @@ -28,6 +28,7 @@ import { ContextMenuTriggerOptions, CustomActionTarget, CustomActionsPosition, + DefaultAppInitData, } from '../types'; import { executeAfterWait, @@ -60,10 +61,12 @@ import { processTrigger } from '../utils/processTrigger'; import { UIPassthroughEvent } from './hostEventClient/contracts'; import * as sessionInfoService from '../utils/sessionInfoService'; import * as authToken from '../authToken'; +import * as apiIntercept from '../api-intercept'; jest.mock('../utils/processTrigger'); const mockProcessTrigger = processTrigger as jest.Mock; +const mockHandleInterceptEvent = jest.spyOn(apiIntercept, 'handleInterceptEvent'); const defaultViewConfig = { frameParams: { width: 1280, @@ -110,6 +113,31 @@ const customVariablesForThirdPartyTools = { key2: '*%^', }; +const getMockAppInitPayload = (data: any) => { + const defaultData: DefaultAppInitData = { + customisations, + authToken: '', + hostConfig: undefined, + runtimeFilterParams: null, + runtimeParameterParams: null, + hiddenHomeLeftNavItems: [], + hiddenHomepageModules: [], + hiddenListColumns: [], + customActions: [], + reorderedHomepageModules: [], + customVariablesForThirdPartyTools, + interceptTimeout: undefined, + interceptUrls: [], + }; + return { + type: EmbedEvent.APP_INIT, + data: { + ...defaultData, + ...data, + }, + }; +} + describe('Unit test case for ts embed', () => { const mockMixPanelEvent = jest.spyOn(mixpanelInstance, 'uploadMixpanelEvent'); beforeEach(() => { @@ -337,22 +365,7 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); @@ -374,22 +387,9 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations: customisationsView, - authToken: '', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + customisations: customisationsView, + })); }); }); @@ -416,22 +416,9 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - hostConfig: undefined, - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [HomepageModule.MyLibrary, HomepageModule.Learning], - hiddenListColumns: [], - customActions: [], - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + hiddenHomepageModules: [HomepageModule.MyLibrary, HomepageModule.Learning], + })); }); }); @@ -455,22 +442,7 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - hostConfig: undefined, - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); @@ -497,23 +469,10 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - hostConfig: undefined, - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - reorderedHomepageModules: - [HomepageModule.MyLibrary, HomepageModule.Watchlist], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + reorderedHomepageModules: + [HomepageModule.MyLibrary, HomepageModule.Watchlist], + })); }); }); @@ -543,22 +502,9 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - runtimeFilterParams: null, - runtimeParameterParams: 'param1=color¶mVal1=blue', - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + runtimeParameterParams: 'param1=color¶mVal1=blue', + })); }); }); @@ -589,22 +535,9 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - runtimeFilterParams: 'col1=color&op1=EQ&val1=blue', - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + runtimeFilterParams: 'col1=color&op1=EQ&val1=blue', + })); }); }); @@ -634,22 +567,7 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); @@ -680,22 +598,7 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); @@ -723,23 +626,10 @@ describe('Unit test case for ts embed', () => { }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: '', - hostConfig: undefined, - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: - [HomeLeftNavItem.Home, HomeLeftNavItem.MonitorSubscription], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - reorderedHomepageModules: [], - customVariablesForThirdPartyTools, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + hiddenHomeLeftNavItems: + [HomeLeftNavItem.Home, HomeLeftNavItem.MonitorSubscription], + })); }); }); @@ -894,22 +784,10 @@ describe('Unit test case for ts embed', () => { postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations, - authToken: 'test_auth_token1', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools: {}, - }, - }); + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + authToken: 'test_auth_token1', + customVariablesForThirdPartyTools: {}, + })); }); jest.spyOn(authService, 'verifyTokenService').mockClear(); @@ -965,36 +843,25 @@ describe('Unit test case for ts embed', () => { }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations: { - content: { - strings: { - Liveboard: 'Dashboard', - }, - stringIDsUrl: 'https://sample-string-ids-url.com', - stringIDs: { - 'liveboard.header.title': 'Dashboard name', - }, + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + customisations: { + content: { + strings: { + Liveboard: 'Dashboard', }, - style: { - customCSS: {}, - customCSSUrl: undefined, + stringIDsUrl: 'https://sample-string-ids-url.com', + stringIDs: { + 'liveboard.header.title': 'Dashboard name', }, }, - authToken: 'test_auth_token1', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [], - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools: {}, + style: { + customCSS: {}, + customCSSUrl: undefined, + }, }, - }); + authToken: 'test_auth_token1', + customVariablesForThirdPartyTools: {}, + })); const customisationContent = mockPort.postMessage.mock.calls[0][0].data.customisations.content; expect(customisationContent.stringIDsUrl) .toBe('https://sample-string-ids-url.com'); @@ -1045,7 +912,7 @@ describe('Unit test case for ts embed', () => { type: EmbedEvent.APP_INIT, data: {}, }; - + // Create a SearchEmbed with valid custom actions to test // CustomActionsValidationResult const searchEmbed = new SearchEmbed(getRootEl(), { @@ -1067,7 +934,7 @@ describe('Unit test case for ts embed', () => { } ] }); - + searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), @@ -1079,44 +946,34 @@ describe('Unit test case for ts embed', () => { }); await executeAfterWait(() => { - expect(mockPort.postMessage).toHaveBeenCalledWith({ - type: EmbedEvent.APP_INIT, - data: { - customisations: { - content: {}, - style: { - customCSS: {}, - customCSSUrl: undefined, - }, + expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ + customisations: { + content: {}, + style: { + customCSS: {}, + customCSSUrl: undefined, }, - authToken: 'test_auth_token1', - runtimeFilterParams: null, - runtimeParameterParams: null, - hiddenHomeLeftNavItems: [], - hiddenHomepageModules: [], - hiddenListColumns: [], - customActions: [ - { - id: 'action2', - name: 'Another Valid Action', - target: CustomActionTarget.VIZ, - position: CustomActionsPosition.MENU, - metadataIds: { vizIds: ['viz456'] } - }, - { - id: 'action1', - name: 'Valid Action', - target: CustomActionTarget.LIVEBOARD, - position: CustomActionsPosition.PRIMARY, - metadataIds: { liveboardIds: ['lb123'] } - } - ], // Actions should be sorted by name - hostConfig: undefined, - reorderedHomepageModules: [], - customVariablesForThirdPartyTools: {}, }, - }); - + authToken: 'test_auth_token1', + customActions: [ + { + id: 'action2', + name: 'Another Valid Action', + target: CustomActionTarget.VIZ, + position: CustomActionsPosition.MENU, + metadataIds: { vizIds: ['viz456'] } + }, + { + id: 'action1', + name: 'Valid Action', + target: CustomActionTarget.LIVEBOARD, + position: CustomActionsPosition.PRIMARY, + metadataIds: { liveboardIds: ['lb123'] } + } + ], // Actions should be sorted by name + customVariablesForThirdPartyTools: {}, + })); + // Verify that CustomActionsValidationResult structure is // correct const appInitData = mockPort.postMessage.mock.calls[0][0].data; @@ -1137,7 +994,7 @@ describe('Unit test case for ts embed', () => { }) ]) ); - + // Verify actions are sorted by name (alphabetically) expect(appInitData.customActions[0].name).toBe('Another Valid Action'); expect(appInitData.customActions[1].name).toBe('Valid Action'); @@ -2444,7 +2301,7 @@ describe('Unit test case for ts embed', () => { }); afterAll((): void => { - window.location = location as any; + (window.location as any) = location; }); it('get url params for TS', () => { @@ -3362,7 +3219,7 @@ describe('Unit test case for ts embed', () => { new Error('Auth failed'), ); const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); await searchEmbed.render(); await executeAfterWait(() => { expect(getRootEl().innerHTML).toContain('Not logged in'); @@ -3371,7 +3228,7 @@ describe('Unit test case for ts embed', () => { window.dispatchEvent(onlineEvent); }).not.toThrow(); }); - + errorSpy.mockReset(); }); @@ -3510,9 +3367,9 @@ describe('Unit test case for ts embed', () => { appEmbed.destroy(); - // Should be called immediately when waitForCleanupOnDestroy is true + // Should be called immediately when config is enabled expect(triggerSpy).toHaveBeenCalledWith(HostEvent.DestroyEmbed); - + // Wait for the timeout to complete await new Promise(resolve => setTimeout(resolve, 1100)); @@ -3535,7 +3392,7 @@ describe('Unit test case for ts embed', () => { await appEmbed.render(); // Mock trigger to resolve quickly (before timeout) - const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockImplementation(() => + const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(null), 100)) ); const removeChildSpy = jest.spyOn(Node.prototype, 'removeChild').mockImplementation(() => getRootEl()); @@ -3565,7 +3422,7 @@ describe('Unit test case for ts embed', () => { await appEmbed.render(); // Mock trigger to take longer than timeout - const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockImplementation(() => + const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(null), 500)) ); const removeChildSpy = jest.spyOn(Node.prototype, 'removeChild').mockImplementation(() => getRootEl()); @@ -3580,4 +3437,395 @@ describe('Unit test case for ts embed', () => { }); }); }); + + describe('handleApiInterceptEvent', () => { + beforeEach(() => { + document.body.innerHTML = getDocumentBody(); + init({ + thoughtSpotHost: 'tshost', + authType: AuthType.None, + }); + jest.clearAllMocks(); + mockHandleInterceptEvent.mockClear(); + }); + + test('should call handleInterceptEvent with correct parameters', async () => { + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: { + method: 'POST', + body: JSON.stringify({ + variables: { + session: { sessionId: 'session-123' }, + contextBookId: 'viz-456' + } + }) + } + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + expect(mockHandleInterceptEvent).toHaveBeenCalledTimes(1); + const call = mockHandleInterceptEvent.mock.calls[0][0]; + expect(call.eventData).toEqual(mockEventData); + expect(call.executeEvent).toBeInstanceOf(Function); + expect(call.getUnsavedAnswerTml).toBeInstanceOf(Function); + expect(call.viewConfig).toMatchObject(defaultViewConfig); + }); + }); + + test('should execute callbacks through executeEvent function', async () => { + let capturedExecuteEvent: any; + mockHandleInterceptEvent.mockImplementation((params) => { + capturedExecuteEvent = params.executeEvent; + }); + + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + const mockCallback = jest.fn(); + searchEmbed.on(EmbedEvent.CustomAction, mockCallback); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + expect(capturedExecuteEvent).toBeDefined(); + + // Simulate executeEvent being called by handleInterceptEvent + const testData = { test: 'data' }; + capturedExecuteEvent(EmbedEvent.CustomAction, testData); + + // executeEvent passes data as first param to callback + expect(mockCallback).toHaveBeenCalled(); + expect(mockCallback.mock.calls[0][0]).toEqual(testData); + }); + }); + + test('should call triggerUIPassThrough through getUnsavedAnswerTml function', async () => { + let capturedGetUnsavedAnswerTml: any; + mockHandleInterceptEvent.mockImplementation((params) => { + capturedGetUnsavedAnswerTml = params.getUnsavedAnswerTml; + }); + + const mockTmlResponse = { tml: 'test-tml-content' }; + mockProcessTrigger.mockResolvedValue([{ value: mockTmlResponse }]); + + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(async () => { + expect(capturedGetUnsavedAnswerTml).toBeDefined(); + + // Clear previous calls + mockProcessTrigger.mockClear(); + + // Simulate getUnsavedAnswerTml being called by + // handleInterceptEvent + const result = await capturedGetUnsavedAnswerTml({ + sessionId: 'session-123', + vizId: 'viz-456' + }); + + expect(mockProcessTrigger).toHaveBeenCalled(); + const callArgs = mockProcessTrigger.mock.calls[0]; + // Verify UIPassthrough event is triggered with the right params + expect(callArgs[1]).toBe('UiPassthrough'); + expect(callArgs[3]).toMatchObject({ + type: 'getUnsavedAnswerTML', + parameters: { + sessionId: 'session-123', + vizId: 'viz-456' + } + }); + expect(result).toEqual(mockTmlResponse); + }); + }); + + test('should pass viewConfig to handleInterceptEvent', async () => { + const customViewConfig = { + ...defaultViewConfig, + interceptUrls: ['/api/test'], + interceptTimeout: 5000, + }; + + const searchEmbed = new SearchEmbed(getRootEl(), customViewConfig); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/api/test', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + const call = mockHandleInterceptEvent.mock.calls[0][0]; + expect(call.viewConfig).toMatchObject({ + interceptUrls: ['/api/test'], + interceptTimeout: 5000, + }); + }); + }); + + test('should handle ApiIntercept event with eventPort', async () => { + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + expect(mockHandleInterceptEvent).toHaveBeenCalled(); + + // Verify the executeEvent function uses the port + const executeEventFn = mockHandleInterceptEvent.mock.calls[0][0].executeEvent; + expect(executeEventFn).toBeDefined(); + }); + }); + + test('should not process non-ApiIntercept events through handleApiInterceptEvent', async () => { + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.Save, + data: { answerId: '123' }, + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + expect(mockHandleInterceptEvent).not.toHaveBeenCalled(); + }); + }); + + test('should handle multiple ApiIntercept events', async () => { + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + await searchEmbed.render(); + + const mockEventData1 = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + const mockEventData2 = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=LoadContextBook', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData1, mockPort); + }); + + await executeAfterWait(() => { + postMessageToParent(getIFrameEl().contentWindow, mockEventData2, mockPort); + }); + + await executeAfterWait(() => { + expect(mockHandleInterceptEvent).toHaveBeenCalledTimes(2); + }); + }); + + test('should pass eventPort to executeCallbacks', async () => { + let capturedExecuteEvent: any; + mockHandleInterceptEvent.mockImplementation((params) => { + capturedExecuteEvent = params.executeEvent; + }); + + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + const mockCallback = jest.fn(); + searchEmbed.on(EmbedEvent.ApiIntercept, mockCallback); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + expect(capturedExecuteEvent).toBeDefined(); + + // Call executeEvent with a response + const responseData = { execute: true }; + capturedExecuteEvent(EmbedEvent.ApiIntercept, responseData); + + // Verify the callback was invoked with the data + expect(mockCallback).toHaveBeenCalled(); + expect(mockCallback.mock.calls[0][0]).toEqual(responseData); + }); + }); + + test('should handle getUnsavedAnswerTml with empty response', async () => { + let capturedGetUnsavedAnswerTml: any; + mockHandleInterceptEvent.mockImplementation((params) => { + capturedGetUnsavedAnswerTml = params.getUnsavedAnswerTml; + }); + + mockProcessTrigger.mockResolvedValue([]); + + const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); + await searchEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=GetChartWithData', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(async () => { + expect(capturedGetUnsavedAnswerTml).toBeDefined(); + + const result = await capturedGetUnsavedAnswerTml({ + sessionId: 'session-123', + vizId: 'viz-456' + }); + + expect(result).toBeUndefined(); + }); + }); + + test('should work with LiveboardEmbed', async () => { + const liveboardEmbed = new LiveboardEmbed(getRootEl(), { + ...defaultViewConfig, + liveboardId: 'test-liveboard-id', + }); + await liveboardEmbed.render(); + + const mockEventData = { + type: EmbedEvent.ApiIntercept, + data: JSON.stringify({ + input: '/prism/?op=LoadContextBook', + init: {} + }) + }; + + const mockPort: any = { + postMessage: jest.fn(), + }; + + await executeAfterWait(() => { + const iframe = getIFrameEl(); + postMessageToParent(iframe.contentWindow, mockEventData, mockPort); + }); + + await executeAfterWait(() => { + expect(mockHandleInterceptEvent).toHaveBeenCalledTimes(1); + expect(mockHandleInterceptEvent).toHaveBeenCalledWith( + expect.objectContaining({ + eventData: mockEventData, + }) + ); + }); + }); + }); }); diff --git a/src/embed/ts-embed.ts b/src/embed/ts-embed.ts index d7c185ef..1687fe5a 100644 --- a/src/embed/ts-embed.ts +++ b/src/embed/ts-embed.ts @@ -71,6 +71,7 @@ import { getEmbedConfig } from './embedConfig'; import { ERROR_MESSAGE } from '../errors'; import { getPreauthInfo } from '../utils/sessionInfoService'; import { HostEventClient } from './hostEventClient/host-event-client'; +import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept'; const { version } = pkgInfo; @@ -201,7 +202,7 @@ export class TsEmbed { }); const embedConfig = getEmbedConfig(); this.embedConfig = embedConfig; - + this.hostEventClient = new HostEventClient(this.iFrame); this.isReadyForRenderPromise = getInitPromise().then(async () => { if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) { @@ -336,33 +337,53 @@ export class TsEmbed { this.subscribedListeners.offline = offlineEventListener; } + private handleApiInterceptEvent({ eventData, eventPort }: { eventData: any, eventPort: MessagePort | void }) { + const executeEvent = (_eventType: EmbedEvent, data: any) => { + this.executeCallbacks(_eventType, data, eventPort); + } + const getUnsavedAnswerTml = async (props: { sessionId?: string, vizId?: string }) => { + const response = await this.triggerUIPassThrough(UIPassthroughEvent.GetUnsavedAnswerTML, props); + return response.filter((item) => item.value)?.[0]?.value; + } + handleInterceptEvent({ eventData, executeEvent, viewConfig: this.viewConfig, getUnsavedAnswerTml }); + } + + private messageEventListener = (event: MessageEvent) => { + const eventType = this.getEventType(event); + const eventPort = this.getEventPort(event); + const eventData = this.formatEventData(event, eventType); + if (event.source === this.iFrame.contentWindow) { + const processedEventData = processEventData( + eventType, + eventData, + this.thoughtSpotHost, + this.isPreRendered ? this.preRenderWrapper : this.el, + ); + + if (eventType === EmbedEvent.ApiIntercept) { + this.handleApiInterceptEvent({ eventData, eventPort }); + return; + } + + this.executeCallbacks( + eventType, + processedEventData, + eventPort, + ); + } + }; /** * Subscribe to message events that depend on successful iframe setup */ private subscribeToMessageEvents() { this.unsubscribeToMessageEvents(); - const messageEventListener = (event: MessageEvent) => { - const eventType = this.getEventType(event); - const eventPort = this.getEventPort(event); - const eventData = this.formatEventData(event, eventType); - if (event.source === this.iFrame.contentWindow) { - this.executeCallbacks( - eventType, - processEventData( - eventType, - eventData, - this.thoughtSpotHost, - this.isPreRendered ? this.preRenderWrapper : this.el, - ), - eventPort, - ); - } - }; - window.addEventListener('message', messageEventListener); + window.addEventListener('message', this.messageEventListener); - this.subscribedListeners.message = messageEventListener; + this.subscribedListeners.message = this.messageEventListener; } + + /** * Adds event listeners for both network and message events. * This maintains backward compatibility with the existing method. @@ -376,6 +397,7 @@ export class TsEmbed { this.subscribeToMessageEvents(); } + private unsubscribeToNetworkEvents() { if (this.subscribedListeners.online) { window.removeEventListener('online', this.subscribedListeners.online); @@ -426,7 +448,7 @@ export class TsEmbed { message: customActionsResult.errors, }); } - return { + const baseInitData = { customisations: getCustomisations(this.embedConfig, this.viewConfig), authToken, runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL @@ -445,7 +467,10 @@ export class TsEmbed { this.embedConfig.customVariablesForThirdPartyTools || {}, hiddenListColumns: this.viewConfig.hiddenListColumns || [], customActions: customActionsResult.actions, + ...getInterceptInitData(this.viewConfig), }; + + return baseInitData; } protected async getAppInitData() { @@ -557,7 +582,7 @@ export class TsEmbed { protected getUpdateEmbedParamsObject() { let queryParams = this.getEmbedParamsObject(); - queryParams = { ...this.viewConfig, ...queryParams }; + queryParams = { ...this.viewConfig, ...queryParams, ...this.getAppInitData() }; return queryParams; } @@ -1023,6 +1048,30 @@ export class TsEmbed { this.iFrame.style.height = getCssDimension(height); } + /** + * We can process the customer given payload before sending it to the embed port + * Embed event handler -> responder -> createEmbedEventResponder -> send response + * @param eventPort The event port for a specific MessageChannel + * @param eventType The event type + * @returns + */ + protected createEmbedEventResponder = (eventPort: MessagePort | void, eventType: EmbedEvent) => { + + const getPayloadToSend = (payload: any) => { + if (eventType === EmbedEvent.OnBeforeGetVizDataIntercept) { + return processLegacyInterceptResponse(payload); + } + if (eventType === EmbedEvent.ApiIntercept) { + return processApiInterceptResponse(payload); + } + return payload; + } + return (payload: any) => { + const payloadToSend = getPayloadToSend(payload); + this.triggerEventOnPort(eventPort, payloadToSend); + } + } + /** * Executes all registered event handlers for a particular event type * @param eventType The event type @@ -1047,9 +1096,8 @@ export class TsEmbed { // payload || (!callbackObj.options.start && dataStatus === embedEventStatus.END) ) { - callbackObj.callback(data, (payload) => { - this.triggerEventOnPort(eventPort, payload); - }); + const responder = this.createEmbedEventResponder(eventPort, eventType); + callbackObj.callback(data, responder); } }); } @@ -1191,12 +1239,12 @@ export class TsEmbed { } } - /** - * @hidden - * Internal state to track if the embed container is loaded. - * This is used to trigger events after the embed container is loaded. - */ - public isEmbedContainerLoaded = false; + /** + * @hidden + * Internal state to track if the embed container is loaded. + * This is used to trigger events after the embed container is loaded. + */ + public isEmbedContainerLoaded = false; /** * @hidden @@ -1348,7 +1396,7 @@ export class TsEmbed { } this.isPreRendered = true; this.showPreRenderByDefault = showPreRenderByDefault; - + const isAlreadyRendered = this.connectPreRendered(); if (isAlreadyRendered && !replaceExistingPreRender) { return this; diff --git a/src/index.ts b/src/index.ts index 9b4c18fd..9d38a3a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,7 @@ import { ListPageColumns, CustomActionsPosition, CustomActionTarget, + InterceptedApiType, } from './types'; import { CustomCssVariables } from './css-variables'; import { SageEmbed, SageViewConfig } from './embed/sage'; @@ -152,6 +153,7 @@ export { DataPanelCustomColumnGroupsAccordionState, CustomActionsPosition, CustomActionTarget, + InterceptedApiType, }; export { resetCachedAuthToken } from './authToken'; diff --git a/src/react/all-types-export.ts b/src/react/all-types-export.ts index edf35b59..eee507bf 100644 --- a/src/react/all-types-export.ts +++ b/src/react/all-types-export.ts @@ -59,6 +59,7 @@ export { resetCachedAuthToken, UIPassthroughEvent, DataPanelCustomColumnGroupsAccordionState, + InterceptedApiType, CustomActionsPosition, CustomActionTarget, } from '../index'; diff --git a/src/types.ts b/src/types.ts index 7a00af03..cd29cc0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -739,7 +739,7 @@ export interface FrameParams { /** * The common configuration object for an embedded view. */ -export interface BaseViewConfig { +export interface BaseViewConfig extends ApiInterceptFlags { /** * @hidden */ @@ -2732,21 +2732,24 @@ export enum EmbedEvent { * Prerequisite: Set `isOnBeforeGetVizDataInterceptEnabled` to `true` * for this embed event to get emitted. - * @param: payload - * @param: responder + * @param:payload The payload received from the embed related to the Data API call. + * @param:responder * Contains elements that lets developers define whether ThoughtSpot * should run the search, and if not, what error message * should be shown to the user. * - * execute: When execute returns `true`, the search will be run. + * `execute` - When execute returns `true`, the search will be run. * When execute returns `false`, the search will not be executed. * - * error: Developers can customize the error message text when `execute` - * returns `false` using the error parameter in responder. + * `error` - Developers can customize the error message text when `execute` + * is `false` using the `errorText` and `errorDescription` parameters in responder. + * + * `errorText` - The error message text to be shown to the user. + * `errorDescription (ThoughtSpot: 10.15.0.cl and above)` - The error description to be shown to the user. * @version SDK : 1.29.0 | ThoughtSpot: 10.3.0.cl * @example *```js - * .on(EmbedEvent.OnBeforeGetVizDataIntercept, + * embed.on(EmbedEvent.OnBeforeGetVizDataIntercept, * (payload, responder) => { * responder({ * data: { @@ -2762,7 +2765,7 @@ export enum EmbedEvent { * ``` * *```js - * .on(EmbedEvent.OnBeforeGetVizDataIntercept, + * embed.on(EmbedEvent.OnBeforeGetVizDataIntercept, * (payload, responder) => { * const query = payload.data.data.answer.search_query * responder({ @@ -2773,7 +2776,8 @@ export enum EmbedEvent { * error: { * //Provide a custom error message to explain to your end user * // why their search did not run, and which searches are accepted by your custom logic. - * errorText: "You can't use this query :" + query + ". + * errorText: "Error Occurred", + * errorDescription: "You can't use this query :" + query + ". * The 'sales' measures can never be used at the 'county' level. * Please try another measure, or remove 'county' from your search." * } @@ -2966,6 +2970,72 @@ export enum EmbedEvent { * @version SDK: 1.41.0 | ThoughtSpot: 10.12.0.cl */ OrgSwitched = 'orgSwitched', + /** + * Emitted when the user intercepts a URL. + * + * Supported on all embed types. + * + * @example + * + * ```js + * embed.on(EmbedEvent.ApiIntercept, (payload, responder) => { + * console.log('payload', payload); + * responder({ + * data: { + * execute: false, + * error: { + * errorText: 'Error Occurred', + * } + * } + * }) + * }) + * ``` + * + * ```js + * // We can also send a response for the intercepted api + * embed.on(EmbedEvent.ApiIntercept, (payload, responder) => { + * console.log('payload', payload); + * responder({ + * data: { + * execute: false, + * response: { + * body: { + * data: { + * // Some api response + * }, + * } + * } + * } + * }) + * }) + * + * // here embed will use the response from the responder as the response for the api + * ``` + * + * ```js + * // We can also send error in response for the intercepted api + * embed.on(EmbedEvent.ApiIntercept, (payload, responder) => { + * console.log('payload', payload); + * responder({ + * data: { + * execute: false, + * response: { + * body: { + * errors: [{ + * title: 'Error Occurred', + * description: 'Error Description', + * isUserError: true, + * }], + * data: {}, + * }, + * } + * } + * }) + * }) + * ``` + * @version SDK: 1.43.0 | ThoughtSpot: 10.15.0.cl + */ + ApiIntercept = 'ApiIntercept', } /** @@ -4275,7 +4345,7 @@ export enum HostEvent { * The different visual modes that the data sources panel within * search could appear in, such as hidden, collapsed, or expanded. */ - + export enum DataSourceVisualMode { /** * The data source panel is hidden. @@ -4295,7 +4365,7 @@ export enum DataSourceVisualMode { * The query params passed down to the embedded ThoughtSpot app * containing configuration and/or visual information. */ - + export enum Param { EmbedApp = 'embedApp', DataSources = 'dataSources', @@ -4452,7 +4522,7 @@ export enum Param { * ``` * See also link:https://developers.thoughtspot.com/docs/actions[Action IDs in the SDK] */ - + export enum Action { /** * The **Save** action on an Answer or Liveboard. @@ -5962,4 +6032,69 @@ export interface DefaultAppInitData { customVariablesForThirdPartyTools: Record; hiddenListColumns: ListPageColumns[]; customActions: CustomAction[]; + interceptTimeout: number | undefined; + interceptUrls: (string | InterceptedApiType)[]; +} + +/** + * Enum for the type of API intercepted + */ +export enum InterceptedApiType { + /** + * The apis that are use to get the data for the embed + */ + AnswerData = 'AnswerData', + /** + * This will intercept all the apis + */ + ALL = 'ALL', + /** + * The apis that are use to get the data for the liveboard + */ + LiveboardData = 'LiveboardData', +} + + +export type ApiInterceptFlags = { + /** + * Flag that allows using `EmbedEvent.OnBeforeGetVizDataIntercept`. + * + * Can be used for Serach and App Embed from SDK 1.29.0 + * + * @version SDK : 1.43.0 | ThoughtSpot: 10.15.0.cl + */ + isOnBeforeGetVizDataInterceptEnabled?: boolean; + /** + * This allows to intercept the urls passed, once intercepted the api will only + * run based on the reponse from the responder of ApiIntercept event. + * + * @example + * ```js + * const embed = new LiveboardEmbed('#embed', { + * ...viewConfig, + * enableApiIntercept: true, + * interceptUrls: [InterceptedApiType.DATA], + * }) + * ``` + * + * @version SDK : 1.43.0 | ThoughtSpot: 10.15.0.cl + */ + interceptUrls?: (string | InterceptedApiType)[]; + /** + * The timeout for the intercept, default is 30000ms + * the api will error out if the timeout is reached + * + * @example + * ```js + * const embed = new LiveboardEmbed('#embed', { + * ...viewConfig, + * enableApiIntercept: true, + * interceptUrls: [InterceptedApiType.ALL], + * interceptTimeout: 1000, + * }) + * ``` + * + * @version SDK : 1.43.0 | ThoughtSpot: 10.15.0.cl + */ + interceptTimeout?: number; } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index d52c6730..86f10dc9 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,4 +1,3 @@ -import { isUndefined } from '../utils'; import { LogLevel } from '../types'; const logFunctions: { @@ -42,7 +41,7 @@ class Logger { public canLog(logLevel: LogLevel): boolean { if (logLevel === LogLevel.SILENT) return false; - if (!isUndefined(globalLogLevelOverride)) { + if (globalLogLevelOverride !== undefined) { return compareLogLevels(globalLogLevelOverride, logLevel) >= 0; } return compareLogLevels(this.logLevel, logLevel) >= 0; diff --git a/src/utils/processData.spec.ts b/src/utils/processData.spec.ts index beb06d81..5f4dc3c6 100644 --- a/src/utils/processData.spec.ts +++ b/src/utils/processData.spec.ts @@ -1,4 +1,3 @@ -import { disable } from 'mixpanel-browser'; import * as processDataInstance from './processData'; import * as answerServiceInstance from './graphql/answerService/answerService'; import * as auth from '../auth'; diff --git a/src/utils/processData.ts b/src/utils/processData.ts index 2d06c5ba..5f34f006 100644 --- a/src/utils/processData.ts +++ b/src/utils/processData.ts @@ -10,7 +10,6 @@ import { AuthType, CustomActionPayload, EmbedEvent } from '../types'; import { AnswerService } from './graphql/answerService/answerService'; import { resetCachedAuthToken } from '../authToken'; import { ERROR_MESSAGE } from '../errors'; -import { logger } from '../utils/logger'; import { handleExitPresentMode } from '../utils'; import { resetCachedPreauthInfo, resetCachedSessionInfo } from './sessionInfoService'; @@ -21,7 +20,7 @@ import { resetCachedPreauthInfo, resetCachedSessionInfo } from './sessionInfoSer function processExitPresentMode(e: any) { const embedConfig = getEmbedConfig(); const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true; - + if (!disableFullscreenPresentation) { handleExitPresentMode(); } @@ -103,7 +102,7 @@ export function processAuthFailure(e: any, containerEl: Element) { const { loginFailedMessage, authType, disableLoginFailurePage, autoLogin, } = getEmbedConfig(); - + const isEmbeddedSSO = authType === AuthType.EmbeddedSSO; const isTrustedAuth = authType === AuthType.TrustedAuthToken || authType === AuthType.TrustedAuthTokenCookieless; const isEmbeddedSSOInfoFailure = isEmbeddedSSO && e?.data?.type === AuthFailureType.UNAUTHENTICATED_FAILURE; @@ -144,26 +143,26 @@ function processAuthLogout(e: any, containerEl: Element) { */ export function processEventData( type: EmbedEvent, - e: any, + eventData: any, thoughtSpotHost: string, containerEl: Element, ): any { switch (type) { case EmbedEvent.CustomAction: - return processCustomAction(e, thoughtSpotHost); + return processCustomAction(eventData, thoughtSpotHost); case EmbedEvent.AuthInit: - return processAuthInit(e); + return processAuthInit(eventData); case EmbedEvent.NoCookieAccess: - return processNoCookieAccess(e, containerEl); + return processNoCookieAccess(eventData, containerEl); case EmbedEvent.AuthFailure: - return processAuthFailure(e, containerEl); + return processAuthFailure(eventData, containerEl); case EmbedEvent.AuthLogout: - return processAuthLogout(e, containerEl); + return processAuthLogout(eventData, containerEl); case EmbedEvent.ExitPresentMode: - return processExitPresentMode(e); + return processExitPresentMode(eventData); case EmbedEvent.CLEAR_INFO_CACHE: return processClearInfoCache(); default: } - return e; + return eventData; }