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

thoughtspot / visual-embed-sdk / #1434

14 Jan 2025 09:34PM UTC coverage: 41.895% (-52.0%) from 93.86%
#1434

Pull #65

Prashant.patil
SCAL-233454-exp checking-mixpanel-error
Pull Request #65: test-exported memb

273 of 1106 branches covered (24.68%)

Branch coverage included in aggregate %.

84 of 356 new or added lines in 14 files covered. (23.6%)

1011 existing lines in 23 files now uncovered.

1301 of 2651 relevant lines covered (49.08%)

9.16 hits per line

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

7.79
/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
    getEncodedQueryParamsString,
16
    getQueryParamString,
17
} from '../utils';
18
import {
13✔
19
    getThoughtSpotHost,
20
    getV2BasePath,
21
} from '../config';
22
import {
13✔
23
    AuthType,
24
    Action,
25
    Param,
26
    EmbedConfig,
27
    ViewConfig,
28
    ContextMenuTriggerOptions,
29
    EmbedEvent,
30
} from '../types';
31
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
13✔
32
import pkgInfo from '../../package.json';
13✔
33
import { getEmbedConfig } from './embedConfig';
13✔
34
import { ERROR_MESSAGE } from '../errors';
13✔
35
import { handleAuth, notifyAuthFailure } from './base';
13✔
36

37
const { version } = pkgInfo;
13✔
38

39
/**
40
 * Global prefix for all Thoughtspot postHash Params.
41
 */
42
export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
13✔
43
const TS_EMBED_ID = '_thoughtspot-embed';
13✔
44

45
/**
46
 * The event id map from v2 event names to v1 event id
47
 * v1 events are the classic embed events implemented in Blink v1
48
 * We cannot rename v1 event types to maintain backward compatibility
49
 * @internal
50
 */
51
const V1EventMap = {};
13✔
52

53
/**
54
 * Base class for embedding v2 experience
55
 * Note: the v2 version of ThoughtSpot Blink is built on the new stack:
56
 * React+GraphQL
57
 */
58
export class BaseEmbed {
13✔
NEW
59
    protected isAppInitialized = false;
×
60

61
    protected viewConfig: ViewConfig;
62

63
    protected embedConfig: EmbedConfig;
64

65
    /**
66
     * The ThoughtSpot hostname or IP address
67
     */
68
    protected thoughtSpotHost: string;
69

70
    /*
71
     * This is the base to access ThoughtSpot V2.
72
     */
73
    protected thoughtSpotV2Base: string;
74

75
    /**
76
     * A flag to mark if an error has occurred.
77
     */
78
    protected isError: boolean;
79

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

NEW
88
    private defaultHiddenActions = [Action.ReportError];
×
89

90
    constructor(viewConfig?: ViewConfig) {
91
        // TODO: handle error
NEW
92
        this.embedConfig = getEmbedConfig();
×
NEW
93
        this.thoughtSpotHost = getThoughtSpotHost(this.embedConfig);
×
NEW
94
        this.thoughtSpotV2Base = getV2BasePath(this.embedConfig);
×
95

NEW
96
        this.isError = false;
×
NEW
97
        this.viewConfig = {
×
98
            excludeRuntimeFiltersfromURL: false,
99
            excludeRuntimeParametersfromURL: false,
100
            ...viewConfig,
101
        };
NEW
102
        this.shouldEncodeUrlQueryParams = this.embedConfig.shouldEncodeUrlQueryParams;
×
NEW
103
        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE, {
×
104
            ...viewConfig,
105
        });
106
    }
107

108
    /**
109
     * Throws error encountered during initialization.
110
     */
111
    protected throwInitError() {
NEW
112
        this.handleError('You need to init the ThoughtSpot SDK module first');
×
113
    }
114

115
    /**
116
     * Handles errors within the SDK
117
     * @param error The error message or object
118
     */
119
    protected handleError(error: string | Record<string, unknown>) {
NEW
120
        this.isError = true;
×
121
    }
122

123
    /**
124
     * Sends updated auth token to the iFrame to avoid user logout
125
     * @param _
126
     * @param responder
127
     */
NEW
128
    protected updateAuthToken = async (_: any, responder: any) => {
×
NEW
129
        const { autoLogin = false, authType } = this.embedConfig; // Set autoLogin default to false
×
NEW
130
        if (authType === AuthType.TrustedAuthTokenCookieless) {
×
NEW
131
            let authToken = '';
×
NEW
132
            try {
×
NEW
133
                authToken = await getAuthenticationToken(this.embedConfig);
×
NEW
134
                responder({
×
135
                    type: EmbedEvent.AuthExpire,
136
                    data: { authToken },
137
                });
138
            } catch (e) {
NEW
139
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
×
140
            }
NEW
141
        } else if (autoLogin) {
×
NEW
142
            handleAuth();
×
143
        }
NEW
144
        notifyAuthFailure(AuthFailureType.EXPIRY);
×
145
    };
146

147
    /**
148
     * Constructs the base URL string to load the ThoughtSpot app.
149
     * @param query
150
     */
151
    protected getEmbedBasePath(query: string): string {
NEW
152
        let queryString = (query.startsWith('?')) ? query : `?${query}`;
×
NEW
153
        if (this.shouldEncodeUrlQueryParams) {
×
NEW
154
            queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(
×
155
                queryString.substr(1),
156
            )}`;
157
        }
NEW
158
        const basePath = [this.thoughtSpotHost, this.thoughtSpotV2Base, queryString]
×
NEW
159
            .filter((x) => x.length > 0)
×
160
            .join('/');
161

NEW
162
        return `${basePath}#`;
×
163
    }
164

165
    /**
166
     * Common query params set for all the embed modes.
167
     * @param queryParams
168
     * @returns queryParams
169
     */
170
    protected getBaseQueryParams(
171
        queryParams: Record<any, any> = {},
×
172
    ) {
NEW
173
        let hostAppUrl = window?.location?.host || '';
×
174

175
        // The below check is needed because TS Cloud firewall, blocks
176
        // localhost/127.0.0.1 in any url param.
NEW
177
        if (hostAppUrl.includes('localhost') || hostAppUrl.includes('127.0.0.1')) {
×
NEW
178
            hostAppUrl = 'local-host';
×
179
        }
NEW
180
        queryParams[Param.EmbedApp] = true;
×
NEW
181
        queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
×
NEW
182
        queryParams[Param.ViewPortHeight] = window.innerHeight;
×
NEW
183
        queryParams[Param.ViewPortWidth] = window.innerWidth;
×
NEW
184
        queryParams[Param.Version] = version;
×
NEW
185
        queryParams[Param.AuthType] = this.embedConfig.authType;
×
NEW
186
        queryParams[Param.blockNonEmbedFullAppAccess] = this.embedConfig.blockNonEmbedFullAppAccess
×
187
            ?? true;
NEW
188
        if (this.embedConfig.disableLoginRedirect === true || this.embedConfig.autoLogin === true) {
×
NEW
189
            queryParams[Param.DisableLoginRedirect] = true;
×
190
        }
NEW
191
        if (this.embedConfig.authType === AuthType.EmbeddedSSO) {
×
NEW
192
            queryParams[Param.ForceSAMLAutoRedirect] = true;
×
193
        }
NEW
194
        if (this.embedConfig.authType === AuthType.TrustedAuthTokenCookieless) {
×
NEW
195
            queryParams[Param.cookieless] = true;
×
196
        }
NEW
197
        if (this.embedConfig.pendoTrackingKey) {
×
NEW
198
            queryParams[Param.PendoTrackingKey] = this.embedConfig.pendoTrackingKey;
×
199
        }
NEW
200
        if (this.embedConfig.numberFormatLocale) {
×
NEW
201
            queryParams[Param.NumberFormatLocale] = this.embedConfig.numberFormatLocale;
×
202
        }
NEW
203
        if (this.embedConfig.dateFormatLocale) {
×
NEW
204
            queryParams[Param.DateFormatLocale] = this.embedConfig.dateFormatLocale;
×
205
        }
NEW
206
        if (this.embedConfig.currencyFormat) {
×
NEW
207
            queryParams[Param.CurrencyFormat] = this.embedConfig.currencyFormat;
×
208
        }
209

210
        const {
211
            disabledActions,
212
            disabledActionReason,
213
            hiddenActions,
214
            visibleActions,
215
            hiddenTabs,
216
            visibleTabs,
217
            showAlerts,
218
            additionalFlags: additionalFlagsFromView,
219
            locale,
220
            customizations,
221
            contextMenuTrigger,
222
            linkOverride,
223
            insertInToSlide,
224
            disableRedirectionLinksInNewTab,
225
            overrideOrgId,
226
            enableFlipTooltipToContextMenu = false,
×
NEW
227
        } = this.viewConfig;
×
228

NEW
229
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
×
230

NEW
231
        const additionalFlags = {
×
232
            ...additionalFlagsFromInit,
233
            ...additionalFlagsFromView,
234
        };
235

NEW
236
        if (enableFlipTooltipToContextMenu) {
×
NEW
237
            queryParams[Param.EnableFlipTooltipToContextMenu] = enableFlipTooltipToContextMenu;
×
238
        }
239

NEW
240
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
×
NEW
241
            this.handleError('You cannot have both hidden actions and visible actions');
×
NEW
242
            return queryParams;
×
243
        }
244

NEW
245
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
×
NEW
246
            this.handleError('You cannot have both hidden Tabs and visible Tabs');
×
NEW
247
            return queryParams;
×
248
        }
249

NEW
250
        if (disabledActions?.length) {
×
NEW
251
            queryParams[Param.DisableActions] = disabledActions;
×
252
        }
NEW
253
        if (disabledActionReason) {
×
NEW
254
            queryParams[Param.DisableActionReason] = disabledActionReason;
×
255
        }
NEW
256
        queryParams[Param.HideActions] = [...this.defaultHiddenActions, ...(hiddenActions ?? [])];
×
NEW
257
        if (Array.isArray(visibleActions)) {
×
NEW
258
            queryParams[Param.VisibleActions] = visibleActions;
×
259
        }
NEW
260
        if (Array.isArray(hiddenTabs)) {
×
NEW
261
            queryParams[Param.HiddenTabs] = hiddenTabs;
×
262
        }
NEW
263
        if (Array.isArray(visibleTabs)) {
×
NEW
264
            queryParams[Param.VisibleTabs] = visibleTabs;
×
265
        }
266
        /**
267
         * Default behavior for context menu will be left-click
268
         *  from version 9.2.0.cl the user have an option to override context
269
         *  menu click
270
         */
NEW
271
        if (contextMenuTrigger === ContextMenuTriggerOptions.LEFT_CLICK) {
×
NEW
272
            queryParams[Param.ContextMenuTrigger] = true;
×
NEW
273
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.RIGHT_CLICK) {
×
NEW
274
            queryParams[Param.ContextMenuTrigger] = false;
×
275
        }
276

NEW
277
        const spriteUrl = customizations?.iconSpriteUrl
×
278
            || this.embedConfig.customizations?.iconSpriteUrl;
×
NEW
279
        if (spriteUrl) {
×
NEW
280
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
×
281
        }
282

NEW
283
        if (showAlerts !== undefined) {
×
NEW
284
            queryParams[Param.ShowAlerts] = showAlerts;
×
285
        }
NEW
286
        if (locale !== undefined) {
×
NEW
287
            queryParams[Param.Locale] = locale;
×
288
        }
289

NEW
290
        if (linkOverride) {
×
NEW
291
            queryParams[Param.LinkOverride] = linkOverride;
×
292
        }
NEW
293
        if (insertInToSlide) {
×
NEW
294
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
×
295
        }
NEW
296
        if (disableRedirectionLinksInNewTab) {
×
NEW
297
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
×
298
        }
NEW
299
        if (overrideOrgId !== undefined) {
×
NEW
300
            queryParams[Param.OverrideOrgId] = overrideOrgId;
×
301
        }
302

NEW
303
        queryParams[Param.OverrideNativeConsole] = true;
×
NEW
304
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
×
305

NEW
306
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
×
NEW
307
            Object.assign(queryParams, additionalFlags);
×
308
        }
309

310
        // Do not add any flags below this, as we want additional flags to
311
        // override other flags
312

NEW
313
        return queryParams;
×
314
    }
315

316
    /**
317
     * Constructs the base URL string to load v1 of the ThoughtSpot app.
318
     * This is used for embedding Liveboards, visualizations, and full application.
319
     * @param queryString The query string to append to the URL.
320
     * @param isAppEmbed A Boolean parameter to specify if you are embedding
321
     * the full application.
322
     */
323
    protected getV1EmbedBasePath(queryString: string): string {
NEW
324
        const queryParams = this.shouldEncodeUrlQueryParams
×
325
            ? `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryString)}`
326
            : `?${queryString}`;
NEW
327
        const host = this.thoughtSpotHost;
×
NEW
328
        const path = `${host}/${queryParams}#`;
×
NEW
329
        return path;
×
330
    }
331

332
    protected getEmbedParams() {
NEW
333
        const queryParams = this.getBaseQueryParams();
×
NEW
334
        return getQueryParamString(queryParams);
×
335
    }
336

337
    protected getRootIframeSrc() {
NEW
338
        const query = this.getEmbedParams();
×
NEW
339
        return this.getEmbedBasePath(query);
×
340
    }
341

342
    /**
343
     * Returns the ThoughtSpot hostname or IP address.
344
     */
345
    protected getThoughtSpotHost(): string {
NEW
346
        return this.thoughtSpotHost;
×
347
    }
348

349
    public getIframeSrc(): string {
NEW
350
        return '';
×
351
    }
352

353
    /**
354
     * Get the Post Url Params for THOUGHTSPOT from the current
355
     * host app URL.
356
     * THOUGHTSPOT URL params starts with a prefix "ts-"
357
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
358
     */
359
    public getThoughtSpotPostUrlParams(
360
        additionalParams: { [key: string]: string | number } = {},
×
361
    ): string {
NEW
362
        const urlHash = window.location.hash;
×
NEW
363
        const queryParams = window.location.search;
×
NEW
364
        const postHashParams = urlHash.split('?');
×
NEW
365
        const postURLParams = postHashParams[postHashParams.length - 1];
×
NEW
366
        const queryParamsObj = new URLSearchParams(queryParams);
×
NEW
367
        const postURLParamsObj = new URLSearchParams(postURLParams);
×
NEW
368
        const params = new URLSearchParams();
×
369

NEW
370
        const addKeyValuePairCb = (value: string, key: string): void => {
×
NEW
371
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
×
NEW
372
                params.append(key, value);
×
373
            }
374
        };
NEW
375
        queryParamsObj.forEach(addKeyValuePairCb);
×
NEW
376
        postURLParamsObj.forEach(addKeyValuePairCb);
×
NEW
377
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
×
378

NEW
379
        let tsParams = params.toString();
×
NEW
380
        tsParams = tsParams ? `?${tsParams}` : '';
×
381

NEW
382
        return tsParams;
×
383
    }
384

385
    /**
386
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
387
     * @version SDK: 1.19.1 | ThoughtSpot: *
388
     */
389
    public destroy(): void {
390
        // no-op
391
    }
392
}
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