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

thoughtspot / visual-embed-sdk / #3033

15 Dec 2025 05:48AM UTC coverage: 94.347% (+0.05%) from 94.3%
#3033

Pull #383

shivam-kumar-ts
SCAL-287502 Add server-side rendering (SSR) guards to embed SDK
Pull Request #383: SCAL-287502 Add server-side rendering (SSR) guards to embed SDK

1370 of 1539 branches covered (89.02%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 2 files covered. (100.0%)

7 existing lines in 2 files now uncovered.

3236 of 3343 relevant lines covered (96.8%)

107.37 hits per line

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

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

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

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

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

65
export let authPromise: Promise<boolean>;
66

67
export const getAuthPromise = (): Promise<boolean> => authPromise;
68

420✔
69
export {
70
    notifyAuthFailure, notifyAuthSDKSuccess, notifyAuthSuccess, notifyLogout,
71
};
24✔
72

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

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

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

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

6✔
152
                    iFrame.classList.add('prefetchIframe');
153
                    iFrame.classList.add(`prefetchIframeNum-${index}`);
6✔
154
                    document.body.appendChild(iFrame);
6✔
155
                },
6✔
156
            );
157
    }
158
};
159

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

175
/**
176
 *
177
 * @param embedConfig
178
 */
179
function backwardCompat(embedConfig: EmbedConfig): EmbedConfig {
180
    const newConfig = { ...embedConfig };
181
    if (embedConfig.noRedirect !== undefined && embedConfig.inPopup === undefined) {
111✔
182
        newConfig.inPopup = embedConfig.noRedirect;
111✔
183
    }
1✔
184
    return newConfig;
185
}
111✔
186

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

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

213
createAndSetInitPromise();
214

215
export const getInitPromise = ():
216
    Promise<
217
      ReturnType<typeof init>
16✔
218
    > => getValueFromWindow<InitFlagStore>(initFlagKey)?.initPromise;
219

16✔
220
export const getIsInitCalled = (): boolean => !!getValueFromWindow(initFlagKey)?.isInitCalled;
221

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

111✔
254
    setGlobalLogLevelOverride(embedConfig.logLevel);
255
    registerReportingObserver();
256

257
    const authEE = new EventEmitter<AuthStatus | AuthEvent>();
258
    setAuthEE(authEE);
259
    handleAuth();
260

261
    const { password, ...configToTrack } = getEmbedConfig();
111✔
262
    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_CALLED_INIT, {
111✔
263
        ...configToTrack,
264
        usedCustomizationSheet: embedConfig.customizations?.style?.customCSSUrl != null,
111✔
265
        usedCustomizationVariables: embedConfig.customizations?.style?.customCSS?.variables != null,
111✔
266
        usedCustomizationRules:
111✔
267
            embedConfig.customizations?.style?.customCSS?.rules_UNSTABLE != null,
268
        usedCustomizationStrings: !!embedConfig.customizations?.content?.strings,
111✔
269
        usedCustomizationIconSprite: !!embedConfig.customizations?.iconSpriteUrl,
111✔
270
    });
271

666✔
272
    if (getEmbedConfig().callPrefetch) {
999✔
273
        prefetch(getEmbedConfig().thoughtSpotHost);
274
    }
999✔
275

666✔
276
    // Resolves the promise created in the initPromiseKey
333✔
277
    getValueFromWindow<InitFlagStore>(initFlagKey).initPromiseResolve(authEE);
278
    getValueFromWindow<InitFlagStore>(initFlagKey).isInitCalled = true;
279

111✔
280
    return authEE as AuthEventEmitter;
2✔
281
};
282

283
/**
284
 *
111✔
285
 */
111✔
286
export function disableAutoLogin(): void {
287
    getEmbedConfig().autoLogin = false;
111✔
288
}
289

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

2✔
311
let renderQueue: Promise<any> = Promise.resolve();
312

2✔
313
/**
2✔
314
 * Renders functions in a queue, resolves to next function only after the callback next
2✔
315
 * is called
316
 * @param fn The function being registered
317
 */
318
export const renderInQueue = (fn: (next?: (val?: any) => void) => Promise<any>): Promise<any> => {
16✔
319
    const { queueMultiRenders = false } = getEmbedConfig();
320
    if (queueMultiRenders) {
321
        renderQueue = renderQueue.then(() => new Promise((res) => fn(res)));
322
        return renderQueue;
323
    }
324
    // Sending an empty function to keep it consistent with the above usage.
325
    return fn(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
16✔
326
};
417✔
327

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

358
    const { thoughtSpotHost, authType } = getEmbedConfig();
16✔
359
    const headers: Record<string, string | undefined> = {
4✔
360
        'Content-Type': 'application/json',
4✔
361
        'x-requested-by': 'ThoughtSpot',
362
    };
1✔
363

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

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

432
    const headers: Record<string, string | undefined> = {
2✔
433
        'Content-Type': 'application/json',
434
        'x-requested-by': 'ThoughtSpot',
4✔
435
    };
4✔
436

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

456
// For testing purposes only
1✔
457
/**
458
 *
459
 */
1✔
460
export function reset(): void {
461
    setEmbedConfig({} as any);
462
    setAuthEE(null);
463
    authPromise = null;
464
}
465

466
/**
467
 * Reloads the ThoughtSpot iframe.
16✔
468
 * @param iFrame
17✔
469
 * @group Global methods
17✔
470
 * @version SDK: 1.43.1
17✔
471
 */
472
export const reloadIframe = (iFrame: HTMLIFrameElement) => {
473
    if (!iFrame) {
474
        logger.warn('reloadIframe called with no iFrame element.');
475
        return;
476
    }
477
    reload(iFrame);
478
};
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