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

thoughtspot / visual-embed-sdk / #1664

13 Mar 2025 03:59PM UTC coverage: 93.874% (-0.2%) from 94.047%
#1664

Pull #152

sastaachar
ci
Pull Request #152: Move init to promise

1015 of 1163 branches covered (87.27%)

Branch coverage included in aggregate %.

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

5 existing lines in 1 file now uncovered.

2540 of 2624 relevant lines covered (96.8%)

63.99 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

16✔
38
const CONFIG_DEFAULTS: Partial<EmbedConfig> = {
39
    loginFailedMessage: 'Not logged in',
16✔
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;
64

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

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

91
const hostUrlToFeatureUrl = {
92
    [PrefetchFeatures.SearchEmbed]: (url: string, flags: string) => `${url}v2/?${flags}#/embed/answer`,
16✔
93
    [PrefetchFeatures.LiveboardEmbed]: (url: string, flags: string) => `${url}?${flags}`,
2✔
94
    [PrefetchFeatures.FullApp]: (url: string, flags: string) => `${url}?${flags}`,
2✔
95
    [PrefetchFeatures.VizEmbed]: (url: string, flags: string) => `${url}?${flags}`,
2✔
UNCOV
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 = (
109
    url?: string,
16✔
110
    prefetchFeatures?: PrefetchFeatures[],
111
    additionalFlags?: { [key: string]: string | number | boolean },
112
): void => {
113
    if (url === '') {
114
        // eslint-disable-next-line no-console
5✔
115
        logger.warn('The prefetch method does not have a valid URL');
116
    } else {
1✔
117
        const features = prefetchFeatures || [PrefetchFeatures.FullApp];
118
        let hostUrl = url || getEmbedConfig().thoughtSpotHost;
4✔
119
        const prefetchFlags = {
4!
120
            [Param.EmbedApp]: true,
4✔
121
            ...getEmbedConfig()?.additionalFlags,
122
            ...additionalFlags,
12!
123
        };
124
        hostUrl = hostUrl[hostUrl.length - 1] === '/' ? hostUrl : `${hostUrl}/`;
125
        Array.from(
4✔
126
            new Set(features
4✔
127
                .map((feature) => hostUrlToFeatureUrl[feature](
128
                    hostUrl,
6✔
129
                    getQueryParamString(prefetchFlags),
130
                ))),
131
        )
132
            .forEach(
133
                (prefetchUrl, index) => {
134
                    const iFrame = document.createElement('iframe');
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
                },
6✔
143
            );
144
    }
145
};
146

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

162
/**
163
 *
164
 * @param embedConfig
165
 */
166
function backwardCompat(embedConfig: EmbedConfig): EmbedConfig {
167
    const newConfig = { ...embedConfig };
168
    if (embedConfig.noRedirect !== undefined && embedConfig.inPopup === undefined) {
60✔
169
        newConfig.inPopup = embedConfig.noRedirect;
60✔
170
    }
1✔
171
    return newConfig;
172
}
60✔
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';
180

16✔
181
export const createAndSetInitPromise = (): void => {
182
    const {
16✔
183
        promise: initPromise,
184
        resolve: initPromiseResolve,
185
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
186
        // @ts-ignore
187
    } = Promise.withResolvers<AuthEventEmitter>();
188
    const initFlagStore: InitFlagStore = {
19✔
189
        initPromise,
19✔
190
        isInitCalled: false,
191
        initPromiseResolve,
192
    };
193
    storeValueInWindow(initFlagKey, initFlagStore, {
194
        // In case of diff imports the promise might be already set
19✔
195
        ignoreIfAlreadyExists: true,
196
    });
197
};
198

199
createAndSetInitPromise();
200

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

280!
206
export const getIsInitCalled = (): boolean => !!getValueFromWindow(initFlagKey)?.isInitCalled;
207

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

239
    setGlobalLogLevelOverride(embedConfig.logLevel);
240
    registerReportingObserver();
60✔
241

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

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

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

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

60✔
265
    return authEE as AuthEventEmitter;
266
};
60✔
267

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

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

296
let renderQueue: Promise<any> = Promise.resolve();
297

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

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

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

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

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

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

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

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