• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thoughtspot / visual-embed-sdk / #3684

01 Apr 2026 03:21AM UTC coverage: 94.247% (-0.01%) from 94.261%
#3684

push

web-flow
added new embed events (#491)

1871 of 2101 branches covered (89.05%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

58 existing lines in 5 files now uncovered.

3502 of 3600 relevant lines covered (97.28%)

123.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.36
/src/embed/base.ts
1
/**
2
 * Copyright (c) 2022
3
 *
4
 * Base classes
5
 * @summary Base classes
6
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
7
 */
8
import EventEmitter from 'eventemitter3';
16✔
9
import { registerReportingObserver } from '../utils/reporting';
16✔
10
import { logger, setGlobalLogLevelOverride } from '../utils/logger';
16✔
11
import { tokenizedFetch } from '../tokenizedFetch';
16✔
12
import { EndPoints } from '../utils/authService/authService';
16✔
13
import { getThoughtSpotHost } from '../config';
16✔
14
import {
16✔
15
    AuthType, EmbedConfig, LogLevel, Param, PrefetchFeatures,
16
} from '../types';
17
import {
16✔
18
    authenticate,
19
    logout as _logout,
20
    AuthFailureType,
21
    AuthStatus,
22
    AuthEvent,
23
    notifyAuthFailure,
24
    notifyAuthSDKSuccess,
25
    notifyAuthSuccess,
26
    notifyLogout,
27
    setAuthEE,
28
    AuthEventEmitter,
29
    postLoginService,
30
} from '../auth';
31
import '../utils/with-resolvers-polyfill';
16✔
32
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
16✔
33
import { getEmbedConfig, setEmbedConfig } from './embedConfig';
16✔
34
import { getQueryParamString, getValueFromWindow, isWindowUndefined, storeValueInWindow } from '../utils';
16✔
35
import { resetAllCachedServices } from '../utils/resetServices';
16✔
36
import { reload } from '../utils/processTrigger';
16✔
37

38
const CONFIG_DEFAULTS: Partial<EmbedConfig> = {
16✔
39
    loginFailedMessage: 'Not logged in',
40
    authTriggerText: 'Authorize',
41
    authType: AuthType.None,
42
    logLevel: LogLevel.ERROR,
43
    waitForCleanupOnDestroy: false,
44
    cleanupTimeout: 5000,
45
};
46

47
export interface executeTMLInput {
48
    metadata_tmls: string[];
49
    import_policy?: 'PARTIAL' | 'ALL_OR_NONE' | 'VALIDATE_ONLY';
50
    create_new?: boolean;
51
}
52

53
export interface exportTMLInput {
54
    metadata: {
55
        identifier: string;
56
        type?: 'LIVEBOARD' | 'ANSWER' | 'LOGICAL_TABLE' | 'CONNECTION';
57
    }[];
58
    export_associated?: boolean;
59
    export_fqn?: boolean;
60
    edoc_format?: 'YAML' | 'JSON';
61
}
62

63
export let authPromise: Promise<boolean>;
64

65
export const getAuthPromise = (): Promise<boolean> => authPromise;
66

67
export {
68
    notifyAuthFailure, notifyAuthSDKSuccess, notifyAuthSuccess, notifyLogout,
69
};
70

71
/**
72
 * Perform authentication on the ThoughtSpot app as applicable.
73
 */
74
export const handleAuth = (): Promise<boolean> => {
75
    authPromise = authenticate(getEmbedConfig());
76
    authPromise.then(
77
        (isLoggedIn) => {
78
            if (!isLoggedIn) {
79
                notifyAuthFailure(AuthFailureType.SDK);
80
            } else {
81
                // Post login service is called after successful login.
82
                postLoginService();
83
                notifyAuthSDKSuccess();
84
            }
85
        },
86
        () => {
87
            notifyAuthFailure(AuthFailureType.SDK);
88
        },
89
    );
90
    return authPromise;
91
};
92

93
const hostUrlToFeatureUrl = {
94
    [PrefetchFeatures.SearchEmbed]: (url: string, flags: string) => `${url}v2/?${flags}#/embed/answer`,
95
    [PrefetchFeatures.LiveboardEmbed]: (url: string, flags: string) => `${url}?${flags}`,
96
    [PrefetchFeatures.FullApp]: (url: string, flags: string) => `${url}?${flags}`,
97
    [PrefetchFeatures.VizEmbed]: (url: string, flags: string) => `${url}?${flags}`,
98
};
99

100
/**
101
 * Prefetches static resources from the specified URL. Web browsers can then cache the
102
 * prefetched resources and serve them from the user's local disk to provide faster access
103
 * to your app.
104
 * @param url The URL provided for prefetch
105
 * @param prefetchFeatures Specify features which needs to be prefetched.
106
 * @param additionalFlags This can be used to add any URL flag.
107
 * @version SDK: 1.4.0 | ThoughtSpot: ts7.sep.cl, 7.2.1
108
 * @group Global methods
109
 */
110
export const prefetch = (
111
    url?: string,
112
    prefetchFeatures?: PrefetchFeatures[],
113
    additionalFlags?: { [key: string]: string | number | boolean },
114
): void => {
115
    if (url === '') {
116
        logger.warn('The prefetch method does not have a valid URL');
117
    } else {
118
        const features = prefetchFeatures || [PrefetchFeatures.FullApp];
119
        let hostUrl = url || getEmbedConfig().thoughtSpotHost;
120
        const prefetchFlags = {
121
            [Param.EmbedApp]: true,
122
            ...getEmbedConfig()?.additionalFlags,
123
            ...additionalFlags,
124
        };
125
        hostUrl = hostUrl[hostUrl.length - 1] === '/' ? hostUrl : `${hostUrl}/`;
126
        Array.from(
127
            new Set(features
128
                .map((feature) => hostUrlToFeatureUrl[feature](
129
                    hostUrl,
130
                    getQueryParamString(prefetchFlags),
131
                ))),
132
        )
133
            .forEach(
134
                (prefetchUrl, index) => {
135
                    const iFrame = document.createElement('iframe');
136
                    iFrame.src = prefetchUrl;
137
                    iFrame.style.width = '0';
138
                    iFrame.style.height = '0';
139
                    iFrame.style.border = '0';
140

141
                    // Make it 'fixed' to keep it in a different stacking
142
                    // context. This should solve the focus behaviours inside
143
                    // the iframe from interfering with main body.
144
                    iFrame.style.position = 'fixed';
145
                    // Push it out of viewport.
146
                    iFrame.style.top = '100vh';
147
                    iFrame.style.left = '100vw';
148

149
                    iFrame.classList.add('prefetchIframe');
150
                    iFrame.classList.add(`prefetchIframeNum-${index}`);
151
                    document.body.appendChild(iFrame);
152
                },
153
            );
154
    }
155
};
156

157
/**
158
 *
159
 * @param embedConfig
160
 */
161
function sanity(embedConfig: EmbedConfig) {
162
    if (embedConfig.thoughtSpotHost === undefined) {
163
        throw new Error('ThoughtSpot host not provided');
164
    }
165
    if (embedConfig.authType === AuthType.TrustedAuthToken) {
166
        if (!embedConfig.authEndpoint && typeof embedConfig.getAuthToken !== 'function') {
167
            throw new Error('Trusted auth should provide either authEndpoint or getAuthToken');
168
        }
169
    }
170
}
171

172
/**
173
 *
174
 * @param embedConfig
175
 */
176
function backwardCompat(embedConfig: EmbedConfig): EmbedConfig {
177
    const newConfig = { ...embedConfig };
178
    if (embedConfig.noRedirect !== undefined && embedConfig.inPopup === undefined) {
179
        newConfig.inPopup = embedConfig.noRedirect;
180
    }
181
    return newConfig;
182
}
183

184
type InitFlagStore = {
185
  initPromise: Promise<ReturnType<typeof init>>;
186
  isInitCalled: boolean;
187
  initPromiseResolve: (value: ReturnType<typeof init>) => void;
188
}
189
const initFlagKey = 'initFlagKey';
190

191
export const createAndSetInitPromise = (): void => {
192
    if (isWindowUndefined()) return;
193
    const {
194
        promise: initPromise,
195
        resolve: initPromiseResolve,
196
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
197
        // @ts-ignore
198
    } = Promise.withResolvers<AuthEventEmitter>();
199
    const initFlagStore: InitFlagStore = {
200
        initPromise,
201
        isInitCalled: false,
202
        initPromiseResolve,
203
    };
204
    storeValueInWindow(initFlagKey, initFlagStore, {
205
        // In case of diff imports the promise might be already set
206
        ignoreIfAlreadyExists: true,
207
    });
208
};
209

210
createAndSetInitPromise();
211

212
export const getInitPromise = ():
213
    Promise<
214
      ReturnType<typeof init>
215
    > => getValueFromWindow<InitFlagStore>(initFlagKey)?.initPromise;
216

217
export const getIsInitCalled = (): boolean => !!getValueFromWindow(initFlagKey)?.isInitCalled;
218

219
/**
220
 * Initializes the Visual Embed SDK globally and perform
221
 * authentication if applicable. This function needs to be called before any ThoughtSpot
222
 * component like Liveboard etc can be embedded. But need not wait for AuthEvent.SUCCESS
223
 * to actually embed. That is handled internally.
224
 * @param embedConfig The configuration object containing ThoughtSpot host,
225
 * authentication mechanism and so on.
226
 * @example
227
 * ```js
228
 *   const authStatus = init({
229
 *     thoughtSpotHost: 'https://my.thoughtspot.cloud',
230
 *     authType: AuthType.None,
231
 *   });
232
 *   authStatus.on(AuthStatus.FAILURE, (reason) => { // do something here });
233
 * ```
234
 * @returns {@link AuthEventEmitter} event emitter which emits events on authentication success,
235
 *      failure and logout. See {@link AuthStatus}
236
 * @version SDK: 1.0.0 | ThoughtSpot ts7.april.cl, 7.2.1
237
 * @group Authentication / Init
238
 */
239
export const init = (embedConfig: EmbedConfig): AuthEventEmitter | null => {
240
    if (isWindowUndefined()) return null;
241
    sanity(embedConfig);
242
    resetAllCachedServices();
243
    embedConfig = setEmbedConfig(
244
        backwardCompat({
245
            ...CONFIG_DEFAULTS,
246
            ...embedConfig,
247
            thoughtSpotHost: getThoughtSpotHost(embedConfig),
248
        }),
249
    );
250

251
    setGlobalLogLevelOverride(embedConfig.logLevel);
252
    registerReportingObserver();
253

481✔
254
    const authEE = new EventEmitter<AuthStatus | AuthEvent>();
255
    setAuthEE(authEE);
256
    handleAuth();
24✔
257

258
    const { password, ...configToTrack } = getEmbedConfig();
259
    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_CALLED_INIT, {
260
        ...configToTrack,
261
        usedCustomizationSheet: embedConfig.customizations?.style?.customCSSUrl != null,
262
        usedCustomizationVariables: embedConfig.customizations?.style?.customCSS?.variables != null,
16✔
263
        usedCustomizationRules:
77✔
264
            embedConfig.customizations?.style?.customCSS?.rules_UNSTABLE != null,
77✔
265
        usedCustomizationStrings: !!embedConfig.customizations?.content?.strings,
266
        usedCustomizationIconSprite: !!embedConfig.customizations?.iconSpriteUrl,
76✔
267
    });
7✔
268

269
    if (getEmbedConfig().callPrefetch) {
270
        prefetch(getEmbedConfig().thoughtSpotHost);
69✔
271
    }
69✔
272

273
    // Resolves the promise created in the initPromiseKey
274
    getValueFromWindow<InitFlagStore>(initFlagKey).initPromiseResolve(authEE);
275
    getValueFromWindow<InitFlagStore>(initFlagKey).isInitCalled = true;
1✔
276

277
    return authEE as AuthEventEmitter;
278
};
77✔
279

280
/**
281
 *
16✔
282
 */
2✔
283
export function disableAutoLogin(): void {
2✔
284
    getEmbedConfig().autoLogin = false;
2✔
UNCOV
285
}
×
286

287
/**
288
 * Logs out from ThoughtSpot. This also sets the autoLogin flag to false, to
289
 * prevent the SDK from automatically logging in again.
290
 *
291
 * You can call the `init` method again to re login, if autoLogin is set to
292
 * true in this second call it will be honored.
293
 * @param doNotDisableAutoLogin This flag when passed will not disable autoLogin
294
 * @returns Promise which resolves when logout completes.
295
 * @version SDK: 1.10.1 | ThoughtSpot: 8.2.0.cl, 8.4.1-sw
296
 * @group Global methods
297
 */
298
export const logout = (doNotDisableAutoLogin = false): Promise<boolean> => {
16✔
299
    if (!doNotDisableAutoLogin) {
300
        disableAutoLogin();
301
    }
302
    return _logout(getEmbedConfig()).then((isLoggedIn) => {
303
        notifyLogout();
5✔
304
        return isLoggedIn;
1✔
305
    });
306
};
4✔
307

4!
308
let renderQueue: Promise<any> = Promise.resolve();
4✔
309

310
/**
12!
311
 * Renders functions in a queue, resolves to next function only after the callback next
312
 * is called
313
 * @param fn The function being registered
4✔
314
 */
4✔
315
export const renderInQueue = (fn: (next?: (val?: any) => void) => Promise<any>): Promise<any> => {
316
    const { queueMultiRenders = false } = getEmbedConfig();
6✔
317
    if (queueMultiRenders) {
318
        renderQueue = renderQueue.then(() => new Promise((res) => fn(res)));
319
        return renderQueue;
320
    }
321
    // Sending an empty function to keep it consistent with the above usage.
322
    return fn(() => {});
323
};
6✔
324

6✔
325
/**
6✔
326
 * Imports TML representation of the metadata objects into ThoughtSpot.
6✔
327
 * @param data
6✔
328
 * @returns imports TML data into ThoughtSpot
329
 * @example
330
 * ```js
331
 *  executeTML({
332
 * //Array of metadata Tmls in string format
6✔
333
 *      metadata_tmls: [
334
 *          "'\''{\"guid\":\"9bd202f5-d431-44bf-9a07-b4f7be372125\",
6✔
335
 *          \"liveboard\":{\"name\":\"Parameters Liveboard\"}}'\''"
6✔
336
 *      ],
337
 *      import_policy: 'PARTIAL', // Specifies the import policy for the TML import.
6✔
338
 *      create_new: false, // If selected, creates TML objects with new GUIDs.
6✔
339
 *  }).then(result => {
6✔
340
 *      console.log(result);
341
 *  }).catch(error => {
342
 *      console.error(error);
343
 *  });
344
 * ```
345
 * @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
346
 * @group Global methods
347
 */
348
export const executeTML = async (data: executeTMLInput): Promise<any> => {
349
    try {
350
        sanity(getEmbedConfig());
131✔
351
    } catch (err) {
2✔
352
        return Promise.reject(err);
353
    }
129✔
354

3✔
355
    const { thoughtSpotHost, authType } = getEmbedConfig();
2✔
356
    const headers: Record<string, string | undefined> = {
357
        'Content-Type': 'application/json',
358
        'x-requested-by': 'ThoughtSpot',
359
    };
360

361
    const payload = {
362
        metadata_tmls: data.metadata_tmls,
363
        import_policy: data.import_policy || 'PARTIAL',
364
        create_new: data.create_new || false,
365
    };
122✔
366
    return tokenizedFetch(`${thoughtSpotHost}${EndPoints.EXECUTE_TML}`, {
122✔
367
        method: 'POST',
1✔
368
        headers,
369
        body: JSON.stringify(payload),
122✔
370
        credentials: 'include',
371
    })
372
        .then((response) => {
373
            if (!response.ok) {
374
                throw new Error(
375
                    `Failed to import TML data: ${response.status} - ${response.statusText}`,
376
                );
377
            }
16✔
378
            return response.json();
379
        })
16✔
380
        .catch((error) => {
28✔
381
            throw error;
382
        });
383
};
384

385
/**
386
 * Exports TML representation of the metadata objects from ThoughtSpot in JSON or YAML
27✔
387
 * format.
27✔
388
 * @param data
389
 * @returns exports TML data
390
 * @example
391
 * ```js
392
 * exportTML({
27✔
393
 *   metadata: [
394
 *     {
395
 *       type: "LIVEBOARD", //Metadata Type
396
 *       identifier: "9bd202f5-d431-44bf-9a07-b4f7be372125" //Metadata Id
397
 *     }
398
 *   ],
16✔
399
 *   export_associated: false,//indicates whether to export associated metadata objects
400
 *   export_fqn: false, //Adds FQNs of the referenced objects.For example, if you are
16✔
401
 *                      //exporting a Liveboard and its associated objects, the API
402
 *                      //returns the Liveboard TML data with the FQNs of the referenced
403
 *                      //worksheet. If the exported TML data includes FQNs, you don't need
548!
404
 *                      //to manually add FQNs of the referenced objects during TML import.
405
 *   edoc_format: "JSON" //It takes JSON or YAML value
493!
406
 * }).then(result => {
407
 *   console.log(result);
408
 * }).catch(error => {
409
 *   console.error(error);
410
 * });
411
 * ```
412
 * @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
413
 * @group Global methods
414
 */
415
export const exportTML = async (data: exportTMLInput): Promise<any> => {
416
    const { thoughtSpotHost, authType } = getEmbedConfig();
417
    try {
418
        sanity(getEmbedConfig());
419
    } catch (err) {
420
        return Promise.reject(err);
421
    }
422
    const payload = {
423
        metadata: data.metadata,
424
        export_associated: data.export_associated || false,
425
        export_fqn: data.export_fqn || false,
426
        edoc_format: data.edoc_format || 'YAML',
427
    };
16✔
428

126✔
429
    const headers: Record<string, string | undefined> = {
125✔
430
        'Content-Type': 'application/json',
122✔
431
        'x-requested-by': 'ThoughtSpot',
122✔
432
    };
433

434
    return tokenizedFetch(`${thoughtSpotHost}${EndPoints.EXPORT_TML}`, {
435
        method: 'POST',
436
        headers,
437
        body: JSON.stringify(payload),
438
        credentials: 'include',
439
    })
122✔
440
        .then((response) => {
122✔
441
            if (!response.ok) {
442
                throw new Error(
122✔
443
                    `Failed to export TML: ${response.status} - ${response.statusText}`,
122✔
444
                );
122✔
445
            }
446
            return response.json();
122✔
447
        })
122✔
448
        .catch((error) => {
449
            throw error;
732✔
450
        });
1,098✔
451
};
452

1,098✔
453
// For testing purposes only
732✔
454
/**
366✔
455
 *
456
 */
457
export function reset(): void {
122✔
458
    setEmbedConfig({} as any);
2✔
459
    setAuthEE(null);
460
    authPromise = null;
461
}
462

122✔
463
/**
122✔
464
 * Reloads the ThoughtSpot iframe.
465
 * @version SDK: 1.43.1
122✔
466
 * @param iFrame
467
 * @group Global methods
468
 */
469
export const reloadIframe = (iFrame: HTMLIFrameElement) => {
470
    if (!iFrame) {
471
        logger.warn('reloadIframe called with no iFrame element.');
16✔
472
        return;
3✔
473
    }
474
    reload(iFrame);
475
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc