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

thoughtspot / visual-embed-sdk / #1480

21 Jan 2025 10:27AM UTC coverage: 38.568% (-2.7%) from 41.223%
#1480

push

Prashant.patil
SCAL-233454-exp auto-attach wv msg handler lint errors

227 of 1153 branches covered (19.69%)

Branch coverage included in aggregate %.

0 of 15 new or added lines in 1 file covered. (0.0%)

445 existing lines in 8 files now uncovered.

1244 of 2661 relevant lines covered (46.75%)

8.91 hits per line

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

5.73
/src/embed/baseEmbed.ts
1
/**
2
 * Copyright (c) 2022
3
 *
4
 * Base classes
5
 * @summary Base classes
6
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
7
 */
8

9
import isEmpty from 'lodash/isEmpty';
13✔
10
import isObject from 'lodash/isObject';
13✔
11
import { AuthFailureType } from '../auth';
13✔
12
import { logger } from '../utils/logger';
13✔
13
import { getAuthenticationToken } from '../authToken';
13✔
14
import {
13✔
15
    getCustomisations,
16
    getEncodedQueryParamsString,
17
    getFilterQuery,
18
    getQueryParamString,
19
    getRuntimeFilters,
20
    getRuntimeParameters,
21
    isMobile,
22
} from '../utils';
23
import {
13✔
24
    getThoughtSpotHost,
25
    getV2BasePath,
26
} from '../config';
27
import {
13✔
28
    AuthType,
29
    Action,
30
    Param,
31
    EmbedConfig,
32
    ViewConfig,
33
    ContextMenuTriggerOptions,
34
    EmbedEvent,
35
    DefaultAppInitData,
36
} from '../types';
37
// import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
38
import pkgInfo from '../../package.json';
13✔
39
import { getEmbedConfig } from './embedConfig';
13✔
40
import { ERROR_MESSAGE } from '../errors';
13✔
41
import { handleAuth, notifyAuthFailure } from './base';
13✔
42

43
const { version } = pkgInfo;
13✔
44

45
/**
46
 * Global prefix for all Thoughtspot postHash Params.
47
 */
48
export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
13✔
49

50
/**
51
 * This is base class From which Mobile and Web parts will derive
52
 * TODO: Add Events handling in base class
53
 * Currently handling URL construct
54
 */
55
export class BaseEmbed {
13✔
UNCOV
56
    protected isAppInitialized = false;
×
57

58
    protected viewConfig: ViewConfig;
59

60
    protected embedConfig: EmbedConfig;
61

62
    /**
63
     * The ThoughtSpot hostname or IP address
64
     */
65
    protected thoughtSpotHost: string;
66

67
    /*
68
     * This is the base to access ThoughtSpot V2.
69
     */
70
    protected thoughtSpotV2Base: string;
71

72
    /**
73
     * A flag to mark if an error has occurred.
74
     */
75
    protected isError: boolean;
76

77
    /**
78
     * Should we encode URL Query Params using base64 encoding which thoughtspot
79
     * will generate for embedding. This provides additional security to
80
     * thoughtspot clusters against Cross site scripting attacks.
81
     * @default false
82
     */
UNCOV
83
    protected shouldEncodeUrlQueryParams = false;
×
84

UNCOV
85
    private defaultHiddenActions = [Action.ReportError];
×
86

87
    constructor(viewConfig?: ViewConfig) {
88
        // TODO: handle error
UNCOV
89
        this.embedConfig = getEmbedConfig();
×
UNCOV
90
        this.thoughtSpotHost = getThoughtSpotHost(this.embedConfig);
×
UNCOV
91
        this.thoughtSpotV2Base = getV2BasePath(this.embedConfig);
×
92

93
        this.isError = false;
×
94
        this.viewConfig = {
×
95
            excludeRuntimeFiltersfromURL: false,
96
            excludeRuntimeParametersfromURL: false,
97
            ...viewConfig,
98
        };
UNCOV
99
        this.shouldEncodeUrlQueryParams = this.embedConfig.shouldEncodeUrlQueryParams;
×
100
    }
101

102
    /**
103
     * Throws error encountered during initialization.
104
     */
105
    protected throwInitError() {
UNCOV
106
        this.handleError('You need to init the ThoughtSpot SDK module first');
×
107
    }
108

109
    /**
110
     * Handles errors within the SDK
111
     * @param error The error message or object
112
     */
113
    protected handleError(error: string | Record<string, unknown>) {
UNCOV
114
        this.isError = true;
×
115
    }
116

117
    protected handleAuthFailure(error: Error) {
UNCOV
118
        throw new Error('Not implemented in child classes');
×
119
    }
120

121
    protected async getAuthTokenForCookielessInit() {
UNCOV
122
        let authToken = '';
×
UNCOV
123
        if (this.embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) return authToken;
×
124

UNCOV
125
        try {
×
UNCOV
126
            authToken = await getAuthenticationToken(this.embedConfig);
×
127
        } catch (error: any) {
128
            this.handleAuthFailure(error);
×
129
            throw error;
×
130
        }
131

132
        return authToken;
×
133
    }
134

135
    protected async getDefaultAppInitData(): Promise<DefaultAppInitData> {
UNCOV
136
        const authToken = await this.getAuthTokenForCookielessInit();
×
UNCOV
137
        return {
×
138
            customisations: getCustomisations(this.embedConfig, this.viewConfig),
139
            authToken,
140
            runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL
×
141
                ? getRuntimeFilters(this.viewConfig.runtimeFilters)
142
                : null,
143
            runtimeParameterParams: this.viewConfig.excludeRuntimeParametersfromURL
×
144
                ? getRuntimeParameters(this.viewConfig.runtimeParameters || [])
×
145
                : null,
146
            hiddenHomepageModules: this.viewConfig.hiddenHomepageModules || [],
×
147
            reorderedHomepageModules: this.viewConfig.reorderedHomepageModules || [],
×
148
            hostConfig: this.embedConfig.hostConfig,
149
            hiddenHomeLeftNavItems: this.viewConfig?.hiddenHomeLeftNavItems
×
150
                ? this.viewConfig?.hiddenHomeLeftNavItems
×
151
                : [],
152
            customVariablesForThirdPartyTools:
153
            this.embedConfig.customVariablesForThirdPartyTools || {},
×
154
        };
155
    }
156

157
    protected async getAppInitData() {
158
        return this.getDefaultAppInitData();
×
159
    }
160

161
    /**
162
     * Sends updated auth token to the iFrame to avoid user logout
163
     * @param _
164
     * @param responder
165
     */
UNCOV
166
    protected updateAuthToken = async (_: any, responder: any) => {
×
UNCOV
167
        const { autoLogin = false, authType } = this.embedConfig; // Set autoLogin default to false
×
UNCOV
168
        if (authType === AuthType.TrustedAuthTokenCookieless) {
×
UNCOV
169
            let authToken = '';
×
UNCOV
170
            try {
×
UNCOV
171
                authToken = await getAuthenticationToken(this.embedConfig);
×
UNCOV
172
                responder({
×
173
                    type: EmbedEvent.AuthExpire,
174
                    data: { authToken },
175
                });
176
            } catch (e) {
177
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
×
178
                this.handleAuthFailure(e);
×
179
            }
180
        } else if (autoLogin) {
×
181
            handleAuth();
×
182
        }
183
        notifyAuthFailure(AuthFailureType.EXPIRY);
×
184
    };
185

186
    /**
187
     * Constructs the base URL string to load the ThoughtSpot app.
188
     * @param query
189
     */
190
    protected getEmbedBasePath(query: string): string {
191
        let queryString = (query.startsWith('?')) ? query : `?${query}`;
×
192
        if (this.shouldEncodeUrlQueryParams) {
×
UNCOV
193
            queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(
×
194
                queryString.substr(1),
195
            )}`;
196
        }
197
        const basePath = [this.thoughtSpotHost, this.thoughtSpotV2Base, queryString]
×
198
            .filter((x) => x.length > 0)
×
199
            .join('/');
200

201
        return `${basePath}#`;
×
202
    }
203

204
    /**
205
     * Common query params set for all the embed modes.
206
     * @param queryParams
207
     * @returns queryParams
208
     */
209
    protected getBaseQueryParams(
210
        queryParams: Record<any, any> = {},
×
211
    ) {
UNCOV
212
        let hostAppUrl = window?.location?.host || '';
×
213

214
        // The below check is needed because TS Cloud firewall, blocks
215
        // localhost/127.0.0.1 in any url param.
UNCOV
216
        if (hostAppUrl.includes('localhost') || hostAppUrl.includes('127.0.0.1')) {
×
UNCOV
217
            hostAppUrl = 'local-host';
×
218
        }
UNCOV
219
        queryParams[Param.EmbedApp] = true;
×
UNCOV
220
        queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
×
UNCOV
221
        queryParams[Param.ViewPortHeight] = window.innerHeight;
×
UNCOV
222
        queryParams[Param.ViewPortWidth] = window.innerWidth;
×
UNCOV
223
        queryParams[Param.Version] = version;
×
UNCOV
224
        queryParams[Param.AuthType] = this.embedConfig.authType;
×
UNCOV
225
        queryParams[Param.blockNonEmbedFullAppAccess] = this.embedConfig.blockNonEmbedFullAppAccess
×
226
            ?? true;
227
        if (this.embedConfig.disableLoginRedirect === true || this.embedConfig.autoLogin === true) {
×
UNCOV
228
            queryParams[Param.DisableLoginRedirect] = true;
×
229
        }
UNCOV
230
        if (this.embedConfig.authType === AuthType.EmbeddedSSO) {
×
231
            queryParams[Param.ForceSAMLAutoRedirect] = true;
×
232
        }
UNCOV
233
        if (this.embedConfig.authType === AuthType.TrustedAuthTokenCookieless) {
×
UNCOV
234
            queryParams[Param.cookieless] = true;
×
235
        }
236
        if (this.embedConfig.pendoTrackingKey) {
×
237
            queryParams[Param.PendoTrackingKey] = this.embedConfig.pendoTrackingKey;
×
238
        }
UNCOV
239
        if (this.embedConfig.numberFormatLocale) {
×
240
            queryParams[Param.NumberFormatLocale] = this.embedConfig.numberFormatLocale;
×
241
        }
242
        if (this.embedConfig.dateFormatLocale) {
×
UNCOV
243
            queryParams[Param.DateFormatLocale] = this.embedConfig.dateFormatLocale;
×
244
        }
245
        if (this.embedConfig.currencyFormat) {
×
246
            queryParams[Param.CurrencyFormat] = this.embedConfig.currencyFormat;
×
247
        }
248

249
        const {
250
            disabledActions,
251
            disabledActionReason,
252
            hiddenActions,
253
            visibleActions,
254
            hiddenTabs,
255
            visibleTabs,
256
            showAlerts,
257
            additionalFlags: additionalFlagsFromView,
258
            locale,
259
            customizations,
260
            contextMenuTrigger,
261
            linkOverride,
262
            insertInToSlide,
263
            disableRedirectionLinksInNewTab,
264
            overrideOrgId,
265
            enableFlipTooltipToContextMenu = false,
×
UNCOV
266
        } = this.viewConfig;
×
267

UNCOV
268
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
×
269

UNCOV
270
        const additionalFlags = {
×
271
            ...additionalFlagsFromInit,
272
            ...additionalFlagsFromView,
273
        };
274

UNCOV
275
        if (enableFlipTooltipToContextMenu) {
×
UNCOV
276
            queryParams[Param.EnableFlipTooltipToContextMenu] = enableFlipTooltipToContextMenu;
×
277
        }
278

279
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
×
280
            this.handleError('You cannot have both hidden actions and visible actions');
×
UNCOV
281
            return queryParams;
×
282
        }
283

284
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
×
UNCOV
285
            this.handleError('You cannot have both hidden Tabs and visible Tabs');
×
286
            return queryParams;
×
287
        }
288

UNCOV
289
        if (disabledActions?.length) {
×
290
            queryParams[Param.DisableActions] = disabledActions;
×
291
        }
UNCOV
292
        if (disabledActionReason) {
×
293
            queryParams[Param.DisableActionReason] = disabledActionReason;
×
294
        }
UNCOV
295
        queryParams[Param.HideActions] = [...this.defaultHiddenActions, ...(hiddenActions ?? [])];
×
296
        if (Array.isArray(visibleActions)) {
×
297
            queryParams[Param.VisibleActions] = visibleActions;
×
298
        }
299
        if (Array.isArray(hiddenTabs)) {
×
300
            queryParams[Param.HiddenTabs] = hiddenTabs;
×
301
        }
UNCOV
302
        if (Array.isArray(visibleTabs)) {
×
303
            queryParams[Param.VisibleTabs] = visibleTabs;
×
304
        }
305
        /**
306
         * Default behavior for context menu will be left-click
307
         *  from version 9.2.0.cl the user have an option to override context
308
         *  menu click
309
         */
UNCOV
310
        if (contextMenuTrigger === ContextMenuTriggerOptions.LEFT_CLICK) {
×
UNCOV
311
            queryParams[Param.ContextMenuTrigger] = true;
×
UNCOV
312
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.RIGHT_CLICK) {
×
313
            queryParams[Param.ContextMenuTrigger] = false;
×
314
        }
315

UNCOV
316
        const spriteUrl = customizations?.iconSpriteUrl
×
317
            || this.embedConfig.customizations?.iconSpriteUrl;
×
UNCOV
318
        if (spriteUrl) {
×
UNCOV
319
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
×
320
        }
321

UNCOV
322
        if (showAlerts !== undefined) {
×
UNCOV
323
            queryParams[Param.ShowAlerts] = showAlerts;
×
324
        }
UNCOV
325
        if (locale !== undefined) {
×
UNCOV
326
            queryParams[Param.Locale] = locale;
×
327
        }
328

329
        if (linkOverride) {
×
UNCOV
330
            queryParams[Param.LinkOverride] = linkOverride;
×
331
        }
UNCOV
332
        if (insertInToSlide) {
×
333
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
×
334
        }
UNCOV
335
        if (disableRedirectionLinksInNewTab) {
×
UNCOV
336
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
×
337
        }
338
        if (overrideOrgId !== undefined) {
×
339
            queryParams[Param.OverrideOrgId] = overrideOrgId;
×
340
        }
341

UNCOV
342
        queryParams[Param.OverrideNativeConsole] = true;
×
UNCOV
343
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
×
344

UNCOV
345
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
×
346
            Object.assign(queryParams, additionalFlags);
×
347
        }
348

349
        // Do not add any flags below this, as we want additional flags to
350
        // override other flags
351

UNCOV
352
        return queryParams;
×
353
    }
354

355
    /**
356
     * Constructs the base URL string to load v1 of the ThoughtSpot app.
357
     * This is used for embedding Liveboards, visualizations, and full application.
358
     * @param queryString The query string to append to the URL.
359
     * @param isAppEmbed A Boolean parameter to specify if you are embedding
360
     * the full application.
361
     */
362
    protected getV1EmbedBasePath(queryString: string): string {
363
        const queryParams = this.shouldEncodeUrlQueryParams
×
364
            ? `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryString)}`
365
            : `?${queryString}`;
366
        const host = this.thoughtSpotHost;
×
367
        const path = `${host}/${queryParams}#`;
×
368
        return path;
×
369
    }
370

371
    protected getEmbedParams() {
372
        const queryParams = this.getBaseQueryParams();
×
UNCOV
373
        return getQueryParamString(queryParams);
×
374
    }
375

376
    protected getRootIframeSrc() {
377
        const query = this.getEmbedParams();
×
UNCOV
378
        return this.getEmbedBasePath(query);
×
379
    }
380

381
    /**
382
     * Returns the ThoughtSpot hostname or IP address.
383
     */
384
    protected getThoughtSpotHost(): string {
UNCOV
385
        return this.thoughtSpotHost;
×
386
    }
387

388
    public getIframeSrc(): string {
UNCOV
389
        return '';
×
390
    }
391

392
    /**
393
     * Get the Post Url Params for THOUGHTSPOT from the current
394
     * host app URL.
395
     * THOUGHTSPOT URL params starts with a prefix "ts-"
396
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
397
     */
398
    public getThoughtSpotPostUrlParams(
399
        additionalParams: { [key: string]: string | number } = {},
×
400
    ): string {
UNCOV
401
        const urlHash = window.location.hash;
×
UNCOV
402
        const queryParams = window.location.search;
×
UNCOV
403
        const postHashParams = urlHash.split('?');
×
UNCOV
404
        const postURLParams = postHashParams[postHashParams.length - 1];
×
UNCOV
405
        const queryParamsObj = new URLSearchParams(queryParams);
×
UNCOV
406
        const postURLParamsObj = new URLSearchParams(postURLParams);
×
UNCOV
407
        const params = new URLSearchParams();
×
408

UNCOV
409
        const addKeyValuePairCb = (value: string, key: string): void => {
×
UNCOV
410
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
×
UNCOV
411
                params.append(key, value);
×
412
            }
413
        };
UNCOV
414
        queryParamsObj.forEach(addKeyValuePairCb);
×
UNCOV
415
        postURLParamsObj.forEach(addKeyValuePairCb);
×
UNCOV
416
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
×
417

UNCOV
418
        let tsParams = params.toString();
×
UNCOV
419
        tsParams = tsParams ? `?${tsParams}` : '';
×
420

UNCOV
421
        return tsParams;
×
422
    }
423

424
    /**
425
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
426
     * @version SDK: 1.19.1 | ThoughtSpot: *
427
     */
428
    public destroy(): void {
429
        // no-op
430
    }
431
}
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