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

thoughtspot / visual-embed-sdk / #1661

12 Mar 2025 05:32PM UTC coverage: 93.853% (-0.2%) from 94.039%
#1661

Pull #152

sastaachar
SCAL-246312 : clean up
Pull Request #152: Move init to promise

1012 of 1160 branches covered (87.24%)

Branch coverage included in aggregate %.

56 of 63 new or added lines in 12 files covered. (88.89%)

6 existing lines in 1 file now uncovered.

2530 of 2614 relevant lines covered (96.79%)

63.55 hits per line

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

93.02
/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 { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
16✔
34
import { getEmbedConfig, setEmbedConfig } from './embedConfig';
16✔
35
import { getQueryParamString, getValueFromWindow, storeValueInWindow } from '../utils';
16✔
36
import { resetAllCachedServices } from '../utils/resetServices';
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
};
44

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

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

61
export let authPromise: Promise<boolean>;
62

63
export const getAuthPromise = (): Promise<boolean> => authPromise;
296✔
64

65
export {
66
    notifyAuthFailure, notifyAuthSDKSuccess, notifyAuthSuccess, notifyLogout,
22✔
67
};
68

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

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

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

147
/**
148
 *
149
 * @param embedConfig
150
 */
151
function sanity(embedConfig: EmbedConfig) {
152
    if (embedConfig.thoughtSpotHost === undefined) {
69✔
153
        throw new Error('ThoughtSpot host not provided');
2✔
154
    }
155
    if (embedConfig.authType === AuthType.TrustedAuthToken) {
67✔
156
        if (!embedConfig.authEndpoint && typeof embedConfig.getAuthToken !== 'function') {
3✔
157
            throw new Error('Trusted auth should provide either authEndpoint or getAuthToken');
2✔
158
        }
159
    }
160
}
161

162
/**
163
 *
164
 * @param embedConfig
165
 */
166
function backwardCompat(embedConfig: EmbedConfig): EmbedConfig {
167
    const newConfig = { ...embedConfig };
60✔
168
    if (embedConfig.noRedirect !== undefined && embedConfig.inPopup === undefined) {
60✔
169
        newConfig.inPopup = embedConfig.noRedirect;
1✔
170
    }
171
    return newConfig;
60✔
172
}
173

174
type InitFlagStore = {
175
  initPromise: Promise<ReturnType<typeof init>>;
176
  isInitCalled: boolean;
177
  initPromiseResolve: (value: ReturnType<typeof init>) => void;
178
}
179
const initFlagKey = 'initFlagKey';
16✔
180

181
export const createAndSetInitPromise = (): void => {
16✔
182
    let initPromiseResolve: (value: ReturnType<typeof init>) => void;
183
    const initPromise = new Promise<ReturnType<typeof init>>((resolve) => {
19✔
184
        initPromiseResolve = resolve;
19✔
185
    });
186
    const initFlagStore: InitFlagStore = {
19✔
187
        initPromise,
188
        isInitCalled: false,
189
        initPromiseResolve,
190
    };
191
    storeValueInWindow(initFlagKey, initFlagStore, {
19✔
192
        // In case of diff imports the promise might be already set
193
        ignoreIfAlreadyExists: true,
194
    });
195
};
196

197
createAndSetInitPromise();
16✔
198

199
export const getInitPromise = ():
16✔
200
    Promise<
201
      ReturnType<typeof init>
202
    > => getValueFromWindow<InitFlagStore>(initFlagKey)?.initPromise;
278!
203

204
export const getIsInitCalled = (): boolean => !!getValueFromWindow(initFlagKey)?.isInitCalled;
271!
205

206
/**
207
 * Initializes the Visual Embed SDK globally and perform
208
 * authentication if applicable. This function needs to be called before any ThoughtSpot
209
 * component like Liveboard etc can be embedded. But need not wait for AuthEvent.SUCCESS
210
 * to actually embed. That is handled internally.
211
 * @param embedConfig The configuration object containing ThoughtSpot host,
212
 * authentication mechanism and so on.
213
 * @example
214
 * ```js
215
 *   const authStatus = init({
216
 *     thoughtSpotHost: 'https://my.thoughtspot.cloud',
217
 *     authType: AuthType.None,
218
 *   });
219
 *   authStatus.on(AuthStatus.FAILURE, (reason) => { // do something here });
220
 * ```
221
 * @returns {@link AuthEventEmitter} event emitter which emits events on authentication success,
222
 *      failure and logout. See {@link AuthStatus}
223
 * @version SDK: 1.0.0 | ThoughtSpot ts7.april.cl, 7.2.1
224
 * @group Authentication / Init
225
 */
226
export const init = (embedConfig: EmbedConfig): AuthEventEmitter => {
16✔
227
    sanity(embedConfig);
63✔
228
    resetAllCachedServices();
60✔
229
    embedConfig = setEmbedConfig(
60✔
230
        backwardCompat({
231
            ...CONFIG_DEFAULTS,
232
            ...embedConfig,
233
            thoughtSpotHost: getThoughtSpotHost(embedConfig),
234
        }),
235
    );
236

237
    setGlobalLogLevelOverride(embedConfig.logLevel);
60✔
238
    registerReportingObserver();
60✔
239

240
    const authEE = new EventEmitter<AuthStatus | AuthEvent>();
60✔
241
    setAuthEE(authEE);
60✔
242
    handleAuth();
60✔
243

244
    const { password, ...configToTrack } = getEmbedConfig();
60✔
245
    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_CALLED_INIT, {
60✔
246
        ...configToTrack,
247
        usedCustomizationSheet: embedConfig.customizations?.style?.customCSSUrl != null,
360✔
248
        usedCustomizationVariables: embedConfig.customizations?.style?.customCSS?.variables != null,
540✔
249
        usedCustomizationRules:
250
            embedConfig.customizations?.style?.customCSS?.rules_UNSTABLE != null,
540✔
251
        usedCustomizationStrings: !!embedConfig.customizations?.content?.strings,
360✔
252
        usedCustomizationIconSprite: !!embedConfig.customizations?.iconSpriteUrl,
180✔
253
    });
254

255
    if (getEmbedConfig().callPrefetch) {
60✔
256
        prefetch(getEmbedConfig().thoughtSpotHost);
2✔
257
    }
258

259
    // Resolves the promise created in the initPromiseKey
260
    getValueFromWindow<InitFlagStore>(initFlagKey).initPromiseResolve(authEE);
60✔
261
    getValueFromWindow<InitFlagStore>(initFlagKey).isInitCalled = true;
60✔
262

263
    return authEE as AuthEventEmitter;
60✔
264
};
265

266
/**
267
 *
268
 */
269
export function disableAutoLogin(): void {
16✔
270
    getEmbedConfig().autoLogin = false;
3✔
271
}
272

273
/**
274
 * Logs out from ThoughtSpot. This also sets the autoLogin flag to false, to
275
 * prevent the SDK from automatically logging in again.
276
 *
277
 * You can call the `init` method again to re login, if autoLogin is set to
278
 * true in this second call it will be honored.
279
 * @param doNotDisableAutoLogin This flag when passed will not disable autoLogin
280
 * @returns Promise which resolves when logout completes.
281
 * @version SDK: 1.10.1 | ThoughtSpot: 8.2.0.cl, 8.4.1-sw
282
 * @group Global methods
283
 */
284
export const logout = (doNotDisableAutoLogin = false): Promise<boolean> => {
16✔
285
    if (!doNotDisableAutoLogin) {
2✔
286
        disableAutoLogin();
2✔
287
    }
288
    return _logout(getEmbedConfig()).then((isLoggedIn) => {
2✔
289
        notifyLogout();
2✔
290
        return isLoggedIn;
2✔
291
    });
292
};
293

294
let renderQueue: Promise<any> = Promise.resolve();
16✔
295

296
/**
297
 * Renders functions in a queue, resolves to next function only after the callback next
298
 * is called
299
 * @param fn The function being registered
300
 */
301
export const renderInQueue = (fn: (next?: (val?: any) => void) => Promise<any>): Promise<any> => {
16✔
302
    const { queueMultiRenders = false } = getEmbedConfig();
261✔
303
    if (queueMultiRenders) {
261!
UNCOV
304
        renderQueue = renderQueue.then(() => new Promise((res) => fn(res)));
×
UNCOV
305
        return renderQueue;
×
306
    }
307
    // Sending an empty function to keep it consistent with the above usage.
308
    return fn(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
261✔
309
};
310

311
/**
312
 * Imports TML representation of the metadata objects into ThoughtSpot.
313
 * @param data
314
 * @returns imports TML data into ThoughtSpot
315
 * @example
316
 * ```js
317
 *  executeTML({
318
 * //Array of metadata Tmls in string format
319
 *      metadata_tmls: [
320
 *          "'\''{\"guid\":\"9bd202f5-d431-44bf-9a07-b4f7be372125\",
321
 *          \"liveboard\":{\"name\":\"Parameters Liveboard\"}}'\''"
322
 *      ],
323
 *      import_policy: 'PARTIAL', // Specifies the import policy for the TML import.
324
 *      create_new: false, // If selected, creates TML objects with new GUIDs.
325
 *  }).then(result => {
326
 *      console.log(result);
327
 *  }).catch(error => {
328
 *      console.error(error);
329
 *  });
330
 *```
331
 * @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
332
 * @group Global methods
333
 */
334
export const executeTML = async (data: executeTMLInput): Promise<any> => {
16✔
335
    try {
4✔
336
        sanity(getEmbedConfig());
4✔
337
    } catch (err) {
338
        return Promise.reject(err);
1✔
339
    }
340

341
    const { thoughtSpotHost, authType } = getEmbedConfig();
3✔
342
    const headers: Record<string, string | undefined> = {
3✔
343
        'Content-Type': 'application/json',
344
        'x-requested-by': 'ThoughtSpot',
345
    };
346

347
    const payload = {
3✔
348
        metadata_tmls: data.metadata_tmls,
349
        import_policy: data.import_policy || 'PARTIAL',
3!
350
        create_new: data.create_new || false,
6✔
351
    };
352
    return tokenizedFetch(`${thoughtSpotHost}${EndPoints.EXECUTE_TML}`, {
3✔
353
        method: 'POST',
354
        headers,
355
        body: JSON.stringify(payload),
356
        credentials: 'include',
357
    })
358
        .then((response) => {
359
            if (!response.ok) {
2!
UNCOV
360
                throw new Error(
×
361
                    `Failed to import TML data: ${response.status} - ${response.statusText}`,
362
                );
363
            }
364
            return response.json();
2✔
365
        })
366
        .catch((error) => {
367
            throw error;
1✔
368
        });
369
};
370

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

415
    const headers: Record<string, string | undefined> = {
2✔
416
        'Content-Type': 'application/json',
417
        'x-requested-by': 'ThoughtSpot',
418
    };
419

420
    return tokenizedFetch(`${thoughtSpotHost}${EndPoints.EXPORT_TML}`, {
2✔
421
        method: 'POST',
422
        headers,
423
        body: JSON.stringify(payload),
424
        credentials: 'include',
425
    })
426
        .then((response) => {
427
            if (!response.ok) {
1!
UNCOV
428
                throw new Error(
×
429
                    `Failed to export TML: ${response.status} - ${response.statusText}`,
430
                );
431
            }
432
            return response.json();
1✔
433
        })
434
        .catch((error) => {
435
            throw error;
1✔
436
        });
437
};
438

439
// For testing purposes only
440
/**
441
 *
442
 */
443
export function reset(): void {
16✔
444
    setEmbedConfig({} as any);
7✔
445
    setAuthEE(null);
7✔
446
    authPromise = null;
7✔
447
}
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