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

thoughtspot / visual-embed-sdk / #3816

05 May 2026 08:14AM UTC coverage: 93.905% (-0.6%) from 94.474%
#3816

push

web-flow
Merge 9a8bbb5d3 into c7ad582cc

1964 of 2218 branches covered (88.55%)

Branch coverage included in aggregate %.

89 of 98 new or added lines in 5 files covered. (90.82%)

8 existing lines in 1 file now uncovered.

3629 of 3738 relevant lines covered (97.08%)

131.96 hits per line

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

88.99
/src/embed/ts-embed.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 isEqual from 'lodash/isEqual';
14✔
10
import isEmpty from 'lodash/isEmpty';
14✔
11
import isObject from 'lodash/isObject';
14✔
12
import {
14✔
13
    TriggerPayload,
14
    TriggerResponse,
15
    UIPassthroughArrayResponse,
16
    UIPassthroughEvent,
17
    UIPassthroughRequest,
18
} from './hostEventClient/contracts';
19
import { logger } from '../utils/logger';
14✔
20
import { getAuthenticationToken } from '../authToken';
14✔
21
import { AnswerService } from '../utils/graphql/answerService/answerService';
14✔
22
import {
14✔
23
    getEncodedQueryParamsString,
24
    getCssDimension,
25
    getOffsetTop,
26
    embedEventStatus,
27
    setAttributes,
28
    getCustomisations,
29
    getRuntimeFilters,
30
    getDOMNode,
31
    getFilterQuery,
32
    getQueryParamString,
33
    getRuntimeParameters,
34
    setStyleProperties,
35
    removeStyleProperties,
36
    isUndefined,
37
    getHostEventsConfig,
38
    getValueFromWindow,
39
} from '../utils';
40
import { getCustomActions } from '../utils/custom-actions';
14✔
41
import {
14✔
42
    getThoughtSpotHost,
43
    URL_MAX_LENGTH,
44
    DEFAULT_EMBED_WIDTH,
45
    DEFAULT_EMBED_HEIGHT,
46
    getV2BasePath,
47
} from '../config';
48
import {
14✔
49
    AuthType,
50
    DOMSelector,
51
    HostEvent,
52
    EmbedEvent,
53
    MessageCallback,
54
    Action,
55
    Param,
56
    EmbedConfig,
57
    MessageOptions,
58
    MessageCallbackObj,
59
    ContextMenuTriggerOptions,
60
    DefaultAppInitData,
61
    AllEmbedViewConfig as ViewConfig,
62
    EmbedErrorDetailsEvent,
63
    ErrorDetailsTypes,
64
    EmbedErrorCodes,
65
    MessagePayload,
66
    ContextType,
67
    ContextObject,
68
} from '../types';
69
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
14✔
70
import { processEventData, processAuthFailure } from '../utils/processData';
14✔
71
import { version } from '../../package.json';
14✔
72
import {
14✔
73
    getAuthPromise, renderInQueue, handleAuth, notifyAuthFailure,
74
    getInitPromise,
75
    getIsInitCalled,
76
    getIsInitCompleted,
77
} from './base';
78
import { AuthFailureType } from '../auth';
14✔
79
import { getEmbedConfig } from './embedConfig';
14✔
80
import { ERROR_MESSAGE } from '../errors';
14✔
81
import { getPreauthInfo } from '../utils/sessionInfoService';
14✔
82
import { HostEventClient } from './hostEventClient/host-event-client';
14✔
83
import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept';
14✔
84

85
/**
86
 * Global prefix for all ThoughtSpot postHash Params.
87
 */
88
export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
14✔
89
const TS_EMBED_ID = '_thoughtspot-embed';
14✔
90

91
/**
92
 * The event id map from v2 event names to v1 event id
93
 * v1 events are the classic embed events implemented in Blink v1
94
 * We cannot rename v1 event types to maintain backward compatibility
95
 * @internal
96
 */
97
const V1EventMap: Record<string, any> = {};
14✔
98

99
/**
100
 * Base class for embedding v2 experience
101
 * Note: the v2 version of ThoughtSpot Blink is built on the new stack:
102
 * React+GraphQL
103
 */
104
export class TsEmbed {
14✔
105
    /**
106
     * The DOM node which was inserted by the SDK to either
107
     * render the iframe or display an error message.
108
     * This is useful for removing the DOM node when the
109
     * embed instance is destroyed.
110
     */
111
    protected insertedDomEl: Node;
112

113
    /**
114
     * The DOM node where the ThoughtSpot app is to be embedded.
115
     */
116
    protected hostElement: HTMLElement;
117

118
    /**
119
     * The key to store the embed instance in the DOM node
120
     */
121
    protected embedNodeKey = '__tsEmbed';
582✔
122

123
    protected isAppInitialized = false;
582✔
124

125
    /**
126
     * A reference to the iframe within which the ThoughtSpot app
127
     * will be rendered.
128
     */
129
    protected iFrame: HTMLIFrameElement;
130

131
    /**
132
     * Setter for the iframe element
133
     * @param {HTMLIFrameElement} iFrame HTMLIFrameElement
134
     */
135
    protected setIframeElement(iFrame: HTMLIFrameElement): void {
136
        this.iFrame = iFrame;
529✔
137
        this.hostEventClient.setIframeElement(iFrame);
529✔
138
    }
139

140
    protected viewConfig: ViewConfig & { visibleTabs?: string[], hiddenTabs?: string[], showAlerts?: boolean };
141

142
    protected embedConfig: EmbedConfig;
143

144
    /**
145
     * The ThoughtSpot hostname or IP address
146
     */
147
    protected thoughtSpotHost: string;
148

149
    /*
150
     * This is the base to access ThoughtSpot V2.
151
     */
152
    protected thoughtSpotV2Base: string;
153

154
    /**
155
     * A map of event handlers for particular message types triggered
156
     * by the embedded app; multiple event handlers can be registered
157
     * against a particular message type.
158
     */
159
    private eventHandlerMap: Map<string, MessageCallbackObj[]>;
160

161
    /**
162
     * A flag that is set to true post render.
163
     */
164
    protected isRendered: boolean;
165

166
    /**
167
     * A flag to mark if an error has occurred.
168
     */
169
    private isError: boolean;
170

171
    /**
172
     * A flag that is set to true post preRender.
173
     */
174
    private isPreRendered: boolean;
175

176
    /**
177
     * Should we encode URL Query Params using base64 encoding which ThoughtSpot
178
     * will generate for embedding. This provides additional security to
179
     * ThoughtSpot clusters against Cross site scripting attacks.
180
     * @default false
181
     */
182
    private shouldEncodeUrlQueryParams = false;
582✔
183

184
    private defaultHiddenActions = [Action.ReportError];
582✔
185

186
    private resizeObserver: ResizeObserver;
187

188
    protected hostEventClient: HostEventClient;
189

190
    protected isReadyForRenderPromise;
191
    protected shouldWaitForRenderPromise: boolean;
192

193
    /**
194
     * Handler for fullscreen change events
195
     */
196
    private fullscreenChangeHandler: (() => void) | null = null;
582✔
197

198
    constructor(domSelector: DOMSelector, viewConfig?: ViewConfig) {
199
        this.hostElement = getDOMNode(domSelector);
582✔
200
        this.eventHandlerMap = new Map();
582✔
201
        this.isError = false;
582✔
202
        this.viewConfig = {
582✔
203
            excludeRuntimeFiltersfromURL: true,
204
            excludeRuntimeParametersfromURL: true,
205
            ...viewConfig,
206
        };
207
        this.registerAppInit();
582✔
208
        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE, {
582✔
209
            ...viewConfig,
210
        });
211

212
        this.hostEventClient = new HostEventClient(this.iFrame);
582✔
213
        this.shouldWaitForRenderPromise = !getIsInitCompleted();
582✔
214
        const afterInit = () => {
582✔
215
            const embedConfig = getEmbedConfig();
582✔
216
            this.embedConfig = embedConfig;
582✔
217
            if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
582✔
218
                this.embedConfig.authTriggerContainer = domSelector;
97✔
219
            }
220
            this.thoughtSpotHost = getThoughtSpotHost(embedConfig);
582✔
221
            this.thoughtSpotV2Base = getV2BasePath(embedConfig);
582✔
222
            this.shouldEncodeUrlQueryParams = embedConfig.shouldEncodeUrlQueryParams;
582✔
223
        }
224
        if (!this.shouldWaitForRenderPromise) {
582✔
225
            afterInit();
578✔
226
        } else {
227
            this.isReadyForRenderPromise = getInitPromise().then(afterInit).finally(() => {
4✔
228
                this.shouldWaitForRenderPromise = true;
4✔
229
            })
230
        }
231
    }
232

233
    /**
234
     * Throws error encountered during initialization.
235
     */
236
    private throwInitError() {
237
        this.handleError({
1✔
238
            errorType: ErrorDetailsTypes.VALIDATION_ERROR,
239
            message: ERROR_MESSAGE.INIT_SDK_REQUIRED,
240
            code: EmbedErrorCodes.INIT_ERROR,
241
            error: ERROR_MESSAGE.INIT_SDK_REQUIRED,
242
        });
243
    }
244

245
    /**
246
     * Handles errors within the SDK
247
     * @param error The error message or object
248
     * @param errorDetails The error details
249
     */
250
    protected handleError(errorDetails: EmbedErrorDetailsEvent) {
251
        this.isError = true;
17✔
252
        this.executeCallbacks(EmbedEvent.Error, errorDetails);
17✔
253
        // Log error
254
        logger.error(errorDetails);
17✔
255
    }
256

257
    /**
258
     * Extracts the type field from the event payload
259
     * @param event The window message event
260
     */
261
    private getEventType(event: MessageEvent) {
262

263
        return event.data?.type || event.data?.__type;
4,728!
264
    }
265

266
    /**
267
     * Extracts the port field from the event payload
268
     * @param event  The window message event
269
     * @returns
270
     */
271
    private getEventPort(event: MessageEvent) {
272
        if (event.ports.length && event.ports[0]) {
4,728✔
273
            return event.ports[0];
3,772✔
274
        }
275
        return null;
956✔
276
    }
277

278
    /**
279
     * Checks if preauth cache is enabled
280
     * from the view config and embed config
281
     * @returns boolean
282
     */
283
    private isPreAuthCacheEnabled() {
284
        // Disable preauth cache when:
285
        // 1. overrideOrgId is present since:
286
        //    - cached auth info would be for wrong org
287
        //    - info call response changes for each different overrideOrgId
288
        // 2. disablePreauthCache is explicitly set to true
289
        // 3. FullAppEmbed has primary navbar visible since:
290
        //    - primary navbar requires fresh auth state for navigation
291
        //    - cached auth may not reflect current user permissions
292
        const isDisabled = (
293
            this.viewConfig.overrideOrgId !== undefined
543✔
294
            || this.embedConfig.disablePreauthCache === true
295
            || this.isFullAppEmbedWithVisiblePrimaryNavbar()
296
        );
297
        return !isDisabled;
543✔
298
    }
299

300
    /**
301
     * Checks if current embed is FullAppEmbed with visible primary navbar
302
     * @returns boolean
303
     */
304
    private isFullAppEmbedWithVisiblePrimaryNavbar(): boolean {
305
        const appViewConfig = this.viewConfig as any;
541✔
306

307
        // Check if this is a FullAppEmbed (AppEmbed)
308
        // showPrimaryNavbar defaults to true if not explicitly set to false
309
        return (
541✔
310
            appViewConfig.embedComponentType === 'AppEmbed'
717✔
311
            && appViewConfig.showPrimaryNavbar === true
312
        );
313
    }
314

315
    /**
316
     * fix for ts7.sep.cl
317
     * will be removed for ts7.oct.cl
318
     * @param event
319
     * @param eventType
320
     * @hidden
321
     */
322
    private formatEventData(event: MessageEvent, eventType: string) {
323
        const eventData = {
4,728✔
324
            ...event.data,
325
            type: eventType,
326
        };
327
        if (!eventData.data) {
4,728✔
328
            eventData.data = event.data.payload;
678✔
329
        }
330
        return eventData;
4,728✔
331
    }
332

333
    private subscribedListeners: Record<string, any> = {};
582✔
334

335
    /**
336
     * Subscribe to network events (online/offline) that should
337
     * work regardless of auth status
338
     */
339
    private subscribeToNetworkEvents() {
340
        this.unsubscribeToNetworkEvents();
518✔
341

342
        const onlineEventListener = (e: Event) => {
518✔
343
            this.trigger(HostEvent.Reload);
280✔
344
        };
345
        window.addEventListener('online', onlineEventListener);
518✔
346

347
        const offlineEventListener = (e: Event) => {
518✔
348
            const errorDetails = {
×
349
                errorType: ErrorDetailsTypes.NETWORK,
350
                message: ERROR_MESSAGE.OFFLINE_WARNING,
351
                code: EmbedErrorCodes.NETWORK_ERROR,
352
                offlineWarning: ERROR_MESSAGE.OFFLINE_WARNING,
353
            };
354
            this.executeCallbacks(EmbedEvent.Error, errorDetails);
×
355
            logger.warn(errorDetails);
×
356
        };
357
        window.addEventListener('offline', offlineEventListener);
518✔
358

359
        this.subscribedListeners.online = onlineEventListener;
518✔
360
        this.subscribedListeners.offline = offlineEventListener;
518✔
361
    }
362

363
    private handleApiInterceptEvent({ eventData, eventPort }: { eventData: any, eventPort: MessagePort | void }) {
364
        const executeEvent = (_eventType: EmbedEvent, data: any) => {
10✔
365
            this.executeCallbacks(_eventType, data, eventPort);
3✔
366
        }
367
        const getUnsavedAnswerTml = async (props: { sessionId?: string, vizId?: string }) => {
10✔
368
            const response = await this.triggerUIPassThrough(UIPassthroughEvent.GetUnsavedAnswerTML, props);
2✔
369
            return response.filter((item) => item.value)?.[0]?.value;
2!
370
        }
371
        handleInterceptEvent({ eventData, executeEvent, viewConfig: this.viewConfig, getUnsavedAnswerTml });
10✔
372
    }
373

374
    private messageEventListener = (event: MessageEvent<any>) => {
582✔
375
        const eventType = this.getEventType(event);
4,728✔
376
        const eventPort = this.getEventPort(event);
4,728✔
377
        const eventData = this.formatEventData(event, eventType);
4,728✔
378
        if (event.source === this.iFrame.contentWindow) {
4,728✔
379
            const processedEventData = processEventData(
98✔
380
                eventType,
381
                eventData,
382
                this.thoughtSpotHost,
383
                this.isPreRendered ? this.preRenderWrapper : this.hostElement,
98✔
384
            );
385

386
            if (eventType === EmbedEvent.ApiIntercept) {
98✔
387
                this.handleApiInterceptEvent({ eventData, eventPort });
10✔
388
                return;
10✔
389
            }
390

391
            this.executeCallbacks(
88✔
392
                eventType,
393
                processedEventData,
394
                eventPort,
395
            );
396
        }
397
    };
398
    /**
399
     * Subscribe to message events that depend on successful iframe setup
400
     */
401
    private subscribeToMessageEvents() {
402
        this.unsubscribeToMessageEvents();
510✔
403

404
        window.addEventListener('message', this.messageEventListener);
510✔
405

406
        this.subscribedListeners.message = this.messageEventListener;
510✔
407
    }
408

409

410
    /**
411
     * Adds event listeners for both network and message events.
412
     * This maintains backward compatibility with the existing method.
413
     * Adds a global event listener to window for "message" events.
414
     * ThoughtSpot detects if a particular event is targeted to this
415
     * embed instance through an identifier contained in the payload,
416
     * and executes the registered callbacks accordingly.
417
     */
418
    private subscribeToEvents() {
419
        this.subscribeToNetworkEvents();
11✔
420
        this.subscribeToMessageEvents();
11✔
421
    }
422

423

424
    private unsubscribeToNetworkEvents() {
425
        if (this.subscribedListeners.online) {
518✔
426
            window.removeEventListener('online', this.subscribedListeners.online);
5✔
427
            delete this.subscribedListeners.online;
5✔
428
        }
429
        if (this.subscribedListeners.offline) {
518✔
430
            window.removeEventListener('offline', this.subscribedListeners.offline);
5✔
431
            delete this.subscribedListeners.offline;
5✔
432
        }
433
    }
434

435
    private unsubscribeToMessageEvents() {
436
        if (this.subscribedListeners.message) {
511✔
437
            window.removeEventListener('message', this.subscribedListeners.message);
6✔
438
            delete this.subscribedListeners.message;
6✔
439
        }
440
    }
441

442
    private unsubscribeToEvents() {
443
        Object.keys(this.subscribedListeners).forEach((key) => {
56✔
444
            window.removeEventListener(key, this.subscribedListeners[key]);
145✔
445
        });
446
    }
447

448
    protected async getAuthTokenForCookielessInit() {
449
        let authToken = '';
50✔
450
        if (this.embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) return authToken;
50✔
451

452
        try {
6✔
453
            authToken = await getAuthenticationToken(this.embedConfig);
6✔
454
        } catch (e) {
455
            processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.hostElement);
2✔
456
            throw e;
2✔
457
        }
458

459
        return authToken;
4✔
460
    }
461

462
    protected async getDefaultAppInitData(): Promise<DefaultAppInitData> {
463
        const authToken = await this.getAuthTokenForCookielessInit();
49✔
464
        const customActionsResult = getCustomActions([
47✔
465
            ...(this.viewConfig.customActions || []),
93✔
466
            ...(this.embedConfig.customActions || [])
94✔
467
        ]);
468
        if (customActionsResult.errors.length > 0) {
47!
469
            this.handleError({
×
470
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
471
                message: customActionsResult.errors,
472
                code: EmbedErrorCodes.CUSTOM_ACTION_VALIDATION,
473
                error: { type: EmbedErrorCodes.CUSTOM_ACTION_VALIDATION, message: customActionsResult.errors }
474
            });
475
        }
476
        const baseInitData = {
47✔
477
            customisations: getCustomisations(this.embedConfig, this.viewConfig),
478
            authToken,
479
            runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL
47✔
480
                ? getRuntimeFilters(this.viewConfig.runtimeFilters)
481
                : null,
482
            runtimeParameterParams: this.viewConfig.excludeRuntimeParametersfromURL
47✔
483
                ? getRuntimeParameters(this.viewConfig.runtimeParameters || [])
91✔
484
                : null,
485
            hiddenHomepageModules: this.viewConfig.hiddenHomepageModules || [],
93✔
486
            reorderedHomepageModules: this.viewConfig.reorderedHomepageModules || [],
93✔
487
            hostConfig: this.embedConfig.hostConfig,
488
            hiddenHomeLeftNavItems: this.viewConfig?.hiddenHomeLeftNavItems
188!
489
                ? this.viewConfig?.hiddenHomeLeftNavItems
3!
490
                : [],
491
            customVariablesForThirdPartyTools:
492
                this.embedConfig.customVariablesForThirdPartyTools || {},
81✔
493
            hiddenListColumns: this.viewConfig.hiddenListColumns || [],
94✔
494
            customActions: customActionsResult.actions,
495
            embedExpiryInAuthToken: this.viewConfig.refreshAuthTokenOnNearExpiry ?? true,
141✔
496
            ...getInterceptInitData(this.viewConfig),
497
            ...getHostEventsConfig(this.viewConfig),
498
        };
499

500
        return baseInitData;
47✔
501
    }
502

503
    protected async getAppInitData() {
504
        return this.getDefaultAppInitData();
49✔
505
    }
506

507
    /**
508
     * Send Custom style as part of payload of APP_INIT
509
     * @param _
510
     * @param responder
511
     */
512
    private appInitCb = async (_: any, responder: any) => {
582✔
513
        try {
38✔
514
            const appInitData = await this.getAppInitData();
38✔
515
            this.isAppInitialized = true;
36✔
516
            responder({
36✔
517
                type: EmbedEvent.APP_INIT,
518
                data: appInitData,
519
            });
520
        } catch (e) {
521
            logger.error(`AppInit failed, Error : ${e?.message}`);
2!
522
        }
523
    };
524

525
    /**
526
     * Helper method to refresh/update auth token for TrustedAuthTokenCookieless auth type
527
     * @param responder - Function to send response back
528
     * @param eventType - The embed event type to send
529
     * @param forceRefresh - Whether to force refresh the token
530
     * @returns Promise that resolves if token was refreshed, rejects otherwise
531
     */
532
    private async refreshAuthTokenForCookieless(
533
        responder: (data: any) => void,
534
        eventType: EmbedEvent,
535
        forceRefresh: boolean = false
×
536
    ): Promise<void> {
537
        const { authType, autoLogin } = this.embedConfig;
14✔
538
        const isAutoLoginTrue = autoLogin ?? (authType === AuthType.TrustedAuthTokenCookieless);
14✔
539

540
        if (isAutoLoginTrue && authType === AuthType.TrustedAuthTokenCookieless) {
14✔
541
            const authToken = await getAuthenticationToken(this.embedConfig, forceRefresh);
8✔
542
            responder({
5✔
543
                type: eventType,
544
                data: { authToken },
545
            });
546
        }
547
    }
548

549
    private handleAuthFailure = (error: Error) => {
582✔
550
        logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${error?.message}`);
4✔
551
        processAuthFailure(error, this.isPreRendered ? this.preRenderWrapper : this.hostElement);
4✔
552
    }
553

554
    /**
555
     * Refresh the auth token if the autoLogin is true and the authType is TrustedAuthTokenCookieless
556
     * @param _
557
     * @param responder
558
     */
559
    private tokenRefresh = async (_: MessagePayload, responder: (data: { type: EmbedEvent, data: { authToken: string } }) => void) => {
582✔
560
        try {
4✔
561
            await this.refreshAuthTokenForCookieless(responder, EmbedEvent.RefreshAuthToken, true);
4✔
562
        } catch (e) {
563
            this.handleAuthFailure(e);
1✔
564
        }
565
    }
566

567
    /**
568
     * Sends updated auth token to the iFrame to avoid user logout
569
     * @param _
570
     * @param responder
571
     */
572
    private updateAuthToken = async (_: MessagePayload, responder: any) => {
582✔
573
        const { authType, autoLogin: autoLoginConfig } = this.embedConfig;
10✔
574
        // Default autoLogin: true for cookieless if undefined/null, otherwise
575
        // false
576
        const autoLogin = autoLoginConfig ?? (authType === AuthType.TrustedAuthTokenCookieless);
10✔
577

578
        try {
10✔
579
            await this.refreshAuthTokenForCookieless(responder, EmbedEvent.AuthExpire, false);
10✔
580
        } catch (e) {
581
            this.handleAuthFailure(e);
2✔
582
        }
583

584
        if (autoLogin && authType !== AuthType.TrustedAuthTokenCookieless) {
10✔
585
            handleAuth();
2✔
586
        }
587
        notifyAuthFailure(AuthFailureType.EXPIRY);
10✔
588
    };
589

590
    /**
591
     * Auto Login and send updated authToken to the iFrame to avoid user session logout
592
     * @param _
593
     * @param responder
594
     */
595
    private idleSessionTimeout = (_: any, responder: any) => {
582✔
596
        handleAuth().then(async () => {
3✔
597
            let authToken = '';
3✔
598
            try {
3✔
599
                authToken = await getAuthenticationToken(this.embedConfig);
3✔
600
                responder({
2✔
601
                    type: EmbedEvent.IdleSessionTimeout,
602
                    data: { authToken },
603
                });
604
            } catch (e) {
605
                this.handleAuthFailure(e);
1✔
606
            }
607
        }).catch((e) => {
608
            logger.error(`Auto Login failed, Error : ${e?.message}`);
×
609
        });
610
        notifyAuthFailure(AuthFailureType.IDLE_SESSION_TIMEOUT);
3✔
611
    };
612

613
    /**
614
     * Register APP_INIT event and sendback init payload
615
     */
616
    private registerAppInit = () => {
582✔
617
        this.on(EmbedEvent.APP_INIT, this.appInitCb, { start: false }, true);
582✔
618
        this.on(EmbedEvent.AuthExpire, this.updateAuthToken, { start: false }, true);
582✔
619
        this.on(EmbedEvent.IdleSessionTimeout, this.idleSessionTimeout, { start: false }, true);
582✔
620

621
        const embedListenerReadyHandler = this.createEmbedContainerHandler(EmbedEvent.EmbedListenerReady);
582✔
622
        this.on(EmbedEvent.EmbedListenerReady, embedListenerReadyHandler, { start: false }, true);
582✔
623

624
        const authInitHandler = this.createEmbedContainerHandler(EmbedEvent.AuthInit);
582✔
625
        this.on(EmbedEvent.AuthInit, authInitHandler, { start: false }, true);
582✔
626
        this.on(EmbedEvent.RefreshAuthToken, this.tokenRefresh, { start: false }, true);
582✔
627
    };
628

629
    /**
630
     * Constructs the base URL string to load the ThoughtSpot app.
631
     * @param query
632
     */
633
    protected getEmbedBasePath(query: string): string {
634
        let queryString = query.startsWith('?') ? query : `?${query}`;
172✔
635
        if (this.shouldEncodeUrlQueryParams) {
172✔
636
            queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(
1✔
637
                queryString.substr(1),
638
            )}`;
639
        }
640
        const basePath = [this.thoughtSpotHost, this.thoughtSpotV2Base, queryString]
172✔
641
            .filter((x) => x.length > 0)
516✔
642
            .join('/');
643

644
        return `${basePath}#`;
172✔
645
    }
646

647
    protected async getUpdateEmbedParamsObject() {
648
        let queryParams = this.getEmbedParamsObject();
11✔
649
        const appInitData = await this.getAppInitData();
11✔
650
        queryParams = { ...this.viewConfig, ...queryParams, ...appInitData };
11✔
651

652
        return queryParams;
11✔
653
    }
654

655
    /**
656
     * Common query params set for all the embed modes.
657
     * @param queryParams
658
     * @returns queryParams
659
     */
660
    protected getBaseQueryParams(queryParams: Record<any, any> = {}) {
178✔
661
        let hostAppUrl = window?.location?.host || '';
534!
662

663
        // The below check is needed because TS Cloud firewall, blocks
664
        // localhost/127.0.0.1 in any url param.
665
        if (hostAppUrl.includes('localhost') || hostAppUrl.includes('127.0.0.1')) {
534!
666
            hostAppUrl = 'local-host';
534✔
667
        }
668
        const blockNonEmbedFullAppAccess = this.embedConfig.blockNonEmbedFullAppAccess ?? true;
534✔
669
        queryParams[Param.EmbedApp] = true;
534✔
670
        queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
534✔
671
        queryParams[Param.ViewPortHeight] = window.innerHeight;
534✔
672
        queryParams[Param.ViewPortWidth] = window.innerWidth;
534✔
673
        queryParams[Param.Version] = version;
534✔
674
        queryParams[Param.AuthType] = this.embedConfig.authType;
534✔
675
        queryParams[Param.blockNonEmbedFullAppAccess] = blockNonEmbedFullAppAccess;
534✔
676
        queryParams[Param.AutoLogin] = this.embedConfig.autoLogin;
534✔
677
        if (this.embedConfig.disableLoginRedirect === true || this.embedConfig.autoLogin === true) {
534✔
678
            queryParams[Param.DisableLoginRedirect] = true;
9✔
679
        }
680
        if (this.embedConfig.authType === AuthType.EmbeddedSSO) {
534!
681
            queryParams[Param.ForceSAMLAutoRedirect] = true;
×
682
        }
683
        if (this.embedConfig.authType === AuthType.TrustedAuthTokenCookieless) {
534✔
684
            queryParams[Param.cookieless] = true;
19✔
685
        }
686
        if (this.embedConfig.pendoTrackingKey) {
534✔
687
            queryParams[Param.PendoTrackingKey] = this.embedConfig.pendoTrackingKey;
1✔
688
        }
689
        if (this.embedConfig.numberFormatLocale) {
534✔
690
            queryParams[Param.NumberFormatLocale] = this.embedConfig.numberFormatLocale;
6✔
691
        }
692
        if (this.embedConfig.dateFormatLocale) {
534✔
693
            queryParams[Param.DateFormatLocale] = this.embedConfig.dateFormatLocale;
6✔
694
        }
695
        if (this.embedConfig.currencyFormat) {
534✔
696
            queryParams[Param.CurrencyFormat] = this.embedConfig.currencyFormat;
6✔
697
        }
698

699
        const {
700
            disabledActions,
701
            disabledActionReason,
702
            hiddenActions,
703
            visibleActions,
704
            hiddenTabs,
705
            visibleTabs,
706
            showAlerts,
707
            additionalFlags: additionalFlagsFromView,
708
            locale,
709
            customizations,
710
            contextMenuTrigger,
711
            linkOverride,
712
            enableLinkOverridesV2,
713
            insertInToSlide,
714
            disableRedirectionLinksInNewTab,
715
            overrideOrgId,
716
            exposeTranslationIDs,
717
            primaryAction,
718
        } = this.viewConfig;
534✔
719

720
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
534✔
721

722
        const additionalFlags = {
534✔
723
            ...additionalFlagsFromInit,
724
            ...additionalFlagsFromView,
725
        };
726

727
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
534✔
728
            this.handleError({
4✔
729
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
730
                message: ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
731
                code: EmbedErrorCodes.CONFLICTING_ACTIONS_CONFIG,
732
                error: ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
733
            });
734
            return queryParams;
4✔
735
        }
736

737
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
530✔
738
            this.handleError({
4✔
739
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
740
                message: ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
741
                code: EmbedErrorCodes.CONFLICTING_TABS_CONFIG,
742
                error: ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
743
            });
744
            return queryParams;
4✔
745
        }
746
        if (primaryAction) {
526!
747
            queryParams[Param.PrimaryAction] = primaryAction;
×
748
        }
749

750
        if (disabledActions?.length) {
526✔
751
            queryParams[Param.DisableActions] = disabledActions;
12✔
752
        }
753
        if (disabledActionReason) {
526✔
754
            queryParams[Param.DisableActionReason] = disabledActionReason;
11✔
755
        }
756
        if (exposeTranslationIDs) {
526✔
757
            queryParams[Param.ExposeTranslationIDs] = exposeTranslationIDs;
1✔
758
        }
759
        queryParams[Param.HideActions] = [...this.defaultHiddenActions, ...(hiddenActions ?? [])];
526✔
760
        if (Array.isArray(visibleActions)) {
526✔
761
            queryParams[Param.VisibleActions] = visibleActions;
6✔
762
        }
763
        if (Array.isArray(hiddenTabs)) {
526✔
764
            queryParams[Param.HiddenTabs] = hiddenTabs;
2✔
765
        }
766
        if (Array.isArray(visibleTabs)) {
526✔
767
            queryParams[Param.VisibleTabs] = visibleTabs;
1✔
768
        }
769
        /**
770
         * Default behavior for context menu will be left-click
771
         *  from version 9.2.0.cl the user have an option to override context
772
         *  menu click
773
         */
774
        if (contextMenuTrigger === ContextMenuTriggerOptions.LEFT_CLICK) {
526✔
775
            queryParams[Param.ContextMenuTrigger] = 'left';
2✔
776
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.RIGHT_CLICK) {
524✔
777
            queryParams[Param.ContextMenuTrigger] = 'right';
1✔
778
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.BOTH_CLICKS) {
523✔
779
            queryParams[Param.ContextMenuTrigger] = 'both';
1✔
780
        }
781

782
        const embedCustomizations = this.embedConfig.customizations;
526✔
783
        const spriteUrl = customizations?.iconSpriteUrl || embedCustomizations?.iconSpriteUrl;
526✔
784
        if (spriteUrl) {
526✔
785
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
1✔
786
        }
787

788
        const stringIDsUrl = customizations?.content?.stringIDsUrl
526✔
789
            || embedCustomizations?.content?.stringIDsUrl;
3,156✔
790
        if (stringIDsUrl) {
526✔
791
            queryParams[Param.StringIDsUrl] = stringIDsUrl;
2✔
792
        }
793

794
        if (showAlerts !== undefined) {
526✔
795
            queryParams[Param.ShowAlerts] = showAlerts;
1✔
796
        }
797
        if (locale !== undefined) {
526✔
798
            queryParams[Param.Locale] = locale;
1✔
799
        }
800
        // TODO: Once V2 is stable, send both flags when
801
        // linkOverride is true (remove the else-if).
802
        if (enableLinkOverridesV2) {
526✔
803
            queryParams[Param.EnableLinkOverridesV2] = true;
1✔
804
            queryParams[Param.LinkOverride] = true;
1✔
805
        } else if (linkOverride) {
525✔
806
            queryParams[Param.LinkOverride] = linkOverride;
1✔
807
        }
808
        if (insertInToSlide) {
526!
809
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
×
810
        }
811
        if (disableRedirectionLinksInNewTab) {
526!
812
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
×
813
        }
814
        if (overrideOrgId !== undefined) {
526✔
815
            queryParams[Param.OverrideOrgId] = overrideOrgId;
3✔
816
        }
817

818
        if (this.isPreAuthCacheEnabled()) {
526✔
819
            queryParams[Param.preAuthCache] = true;
517✔
820
        }
821

822
        queryParams[Param.OverrideNativeConsole] = true;
526✔
823
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
526✔
824

825
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
526✔
826
            Object.assign(queryParams, additionalFlags);
10✔
827
        }
828

829
        // Do not add any flags below this, as we want additional flags to
830
        // override other flags
831

832
        return queryParams;
526✔
833
    }
834

835
    /**
836
     * Constructs the base URL string to load v1 of the ThoughtSpot app.
837
     * This is used for embedding Liveboards, visualizations, and full application.
838
     * @param queryString The query string to append to the URL.
839
     * @param isAppEmbed A Boolean parameter to specify if you are embedding
840
     * the full application.
841
     */
842
    protected getV1EmbedBasePath(queryString: string): string {
843
        const queryParams = this.shouldEncodeUrlQueryParams
344✔
844
            ? `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryString)}`
845
            : `?${queryString}`;
846
        const host = this.thoughtSpotHost;
344✔
847
        const path = `${host}/${queryParams}#`;
344✔
848
        return path;
344✔
849
    }
850

851
    protected getEmbedParams() {
852
        const queryParams = this.getEmbedParamsObject();
×
853
        return getQueryParamString(queryParams);
×
854
    }
855

856
    protected getEmbedParamsObject() {
857
        const params = this.getBaseQueryParams();
7✔
858
        return params;
7✔
859
    }
860

861
    protected getRootIframeSrc() {
862
        const query = this.getEmbedParams();
121✔
863
        return this.getEmbedBasePath(query);
121✔
864
    }
865

866
    protected createIframeEl(frameSrc: string): HTMLIFrameElement {
867
        const iFrame = document.createElement('iframe');
490✔
868

869
        iFrame.src = frameSrc;
490✔
870
        iFrame.id = TS_EMBED_ID;
490✔
871
        iFrame.setAttribute('data-ts-iframe', 'true');
490✔
872

873
        // according to screenfull.js documentation
874
        // allowFullscreen, webkitallowfullscreen and mozallowfullscreen must be
875
        // true
876
        iFrame.allowFullscreen = true;
490✔
877
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
878
        // @ts-ignore
879
        iFrame.webkitallowfullscreen = true;
490✔
880
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
881
        // @ts-ignore
882
        iFrame.mozallowfullscreen = true;
490✔
883
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
884
        // @ts-ignore
885
        iFrame.allow = 'clipboard-read; clipboard-write; fullscreen; local-network-access;';
490✔
886

887
        const frameParams = this.viewConfig.frameParams;
490✔
888
        const { height: frameHeight, width: frameWidth, ...restParams } = frameParams || {};
490✔
889
        const width = getCssDimension(frameWidth || DEFAULT_EMBED_WIDTH);
490✔
890
        const height = getCssDimension(frameHeight || DEFAULT_EMBED_HEIGHT);
490✔
891
        setAttributes(iFrame, restParams);
490✔
892

893
        iFrame.style.width = `${width}`;
490✔
894
        iFrame.style.height = `${height}`;
490✔
895
        // Set minimum height to the frame so that,
896
        // scaling down on the fullheight doesn't make it too small.
897
        iFrame.style.minHeight = `${height}`;
490✔
898
        iFrame.style.border = '0';
490✔
899
        iFrame.name = 'ThoughtSpot Embedded Analytics';
490✔
900
        return iFrame;
490✔
901
    }
902

903
    /**
904
     * Returns true if this embed instance is configured for pre-rendering.
905
     */
906
    protected isPreRenderEmbed() {
907
        return !!this.viewConfig.preRenderId;
507✔
908
    }
909
    protected handleInsertionIntoDOM(child: string | Node): void {
910
        if (this.isPreRenderEmbed()) {
507✔
911
            this.insertIntoDOMForPreRender(child);
21✔
912
        } else {
913
            this.insertIntoDOM(child);
486✔
914
        }
915
        if (this.insertedDomEl instanceof Node) {
507✔
916
            (this.insertedDomEl as any)[this.embedNodeKey] = this;
482✔
917
        }
918
        if (this.preRenderWrapper) {
507✔
919
            (this.preRenderWrapper as any)[this.embedNodeKey] = this;
21✔
920
        }
921
    }
922

923
    /**
924
     * Renders the embedded ThoughtSpot app in an iframe and sets up
925
     * event listeners.
926
     * @param url - The URL of the embedded ThoughtSpot app.
927
     */
928
    protected async renderIFrame(url: string): Promise<any> {
929
        if (this.isError) {
515✔
930
            return null;
8✔
931
        }
932
        if (!this.thoughtSpotHost) {
507✔
933
            this.throwInitError();
1✔
934
        }
935
        if (url.length > URL_MAX_LENGTH) {
507!
936
            // warn: The URL is too long
937
        }
938

939
        return renderInQueue((nextInQueue) => {
507✔
940
            const initTimestamp = Date.now();
507✔
941

942
            this.executeCallbacks(EmbedEvent.Init, {
507✔
943
                data: {
944
                    timestamp: initTimestamp,
945
                },
946
                type: EmbedEvent.Init,
947
            });
948

949
            uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
507✔
950

951
            // Always subscribe to network events, regardless of auth status
952
            this.subscribeToNetworkEvents();
507✔
953

954
            return getAuthPromise()
507!
955
                ?.then((isLoggedIn: boolean) => {
956
                    if (!isLoggedIn) {
502✔
957
                        this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
3✔
958
                        return;
3✔
959
                    }
960

961
                    this.setIframeElement(this.iFrame || this.createIframeEl(url));
499✔
962
                    this.iFrame.addEventListener('load', () => {
499✔
963
                        nextInQueue();
17✔
964
                        const loadTimestamp = Date.now();
17✔
965
                        this.executeCallbacks(EmbedEvent.Load, {
17✔
966
                            data: {
967
                                timestamp: loadTimestamp,
968
                            },
969
                            type: EmbedEvent.Load,
970
                        });
971
                        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_COMPLETE, {
17✔
972
                            elWidth: this.iFrame.clientWidth,
973
                            elHeight: this.iFrame.clientHeight,
974
                            timeTookToLoad: loadTimestamp - initTimestamp,
975
                        });
976
                        // Send info event  if preauth cache is enabled
977
                        if (this.isPreAuthCacheEnabled()) {
17✔
978
                            getPreauthInfo().then((data) => {
13✔
979
                                if (data?.info) {
13✔
980
                                    this.trigger(HostEvent.InfoSuccess, data);
5✔
981
                                }
982
                            });
983
                        }
984

985
                        // Setup fullscreen change handler after iframe is
986
                        // loaded and ready
987
                        this.setupFullscreenChangeHandler();
17✔
988
                    });
989
                    this.iFrame.addEventListener('error', () => {
499✔
990
                        nextInQueue();
1✔
991
                    });
992
                    this.handleInsertionIntoDOM(this.iFrame);
499✔
993
                    const prefetchIframe = document.querySelectorAll('.prefetchIframe');
499✔
994
                    if (prefetchIframe.length) {
499!
995
                        prefetchIframe.forEach((el) => {
×
996
                            el.remove();
×
997
                        });
998
                    }
999
                    // Subscribe to message events only after successful
1000
                    // auth and iframe setup
1001
                    this.subscribeToMessageEvents();
499✔
1002
                })
1003
                .catch((error) => {
1004
                    nextInQueue();
5✔
1005
                    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_FAILED, {
5✔
1006
                        error: JSON.stringify(error),
1007
                    });
1008
                    this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
5✔
1009
                    this.handleError({
5✔
1010
                        errorType: ErrorDetailsTypes.API,
1011
                        message: error.message || ERROR_MESSAGE.LOGIN_FAILED,
6✔
1012
                        code: EmbedErrorCodes.LOGIN_FAILED,
1013
                        error: error,
1014
                    });
1015
                });
1016
        });
1017
    }
1018

1019
    protected createPreRenderWrapper(): HTMLDivElement {
1020
        const preRenderIds = this.getPreRenderIds();
21✔
1021

1022
        document.getElementById(preRenderIds.wrapper)?.remove();
21✔
1023

1024
        const preRenderWrapper = document.createElement('div');
21✔
1025
        preRenderWrapper.id = preRenderIds.wrapper;
21✔
1026
        const initialPreRenderWrapperStyle = {
21✔
1027
            position: 'absolute',
1028
            top: '0',
1029
            left: '0',
1030
            width: '100vw',
1031
            height: '100vh',
1032
        };
1033
        setStyleProperties(preRenderWrapper, initialPreRenderWrapperStyle);
21✔
1034

1035
        return preRenderWrapper;
21✔
1036
    }
1037

1038
    protected preRenderWrapper: HTMLElement;
1039

1040
    protected preRenderChild: HTMLElement;
1041

1042
    /**
1043
     * Checks for an existing pre-rendered component and connects to it.
1044
     *
1045
     * If a matching pre-rendered component is found in the DOM, this method
1046
     * sets the internal properties of the embed object to reference it.
1047
     *
1048
     * @returns True if a connection was successfully established, false otherwise.
1049
     */
1050
    protected connectPreRendered(): boolean {
1051
        const preRenderIds = this.getPreRenderIds();
28✔
1052
        const preRenderWrapperElement = document.getElementById(preRenderIds.wrapper);
28✔
1053
        this.preRenderWrapper = this.preRenderWrapper || preRenderWrapperElement;
28✔
1054

1055
        this.preRenderChild = this.preRenderChild || document.getElementById(preRenderIds.child);
28✔
1056

1057
        if (this.preRenderWrapper && this.preRenderChild) {
28✔
1058
            this.isPreRendered = true;
10✔
1059
            if (this.preRenderChild instanceof HTMLIFrameElement) {
10!
1060
                this.setIframeElement(this.preRenderChild);
10✔
1061
            }
1062
            this.isRendered = true;
10✔
1063
        }
1064

1065
        return this.isPreRenderConnected();
28✔
1066
    }
1067

1068
    protected isPreRenderConnected(): boolean {
1069
        return (
83✔
1070
            Boolean(this.preRenderWrapper && this.preRenderChild)
137✔
1071
        );
1072
    }
1073

1074
    protected createPreRenderChild(child: string | Node): HTMLElement {
1075
        const preRenderIds = this.getPreRenderIds();
21✔
1076

1077
        document.getElementById(preRenderIds.child)?.remove();
21✔
1078

1079
        if (child instanceof HTMLElement) {
21✔
1080
            child.id = preRenderIds.child;
20✔
1081
            return child;
20✔
1082
        }
1083

1084
        const divChildNode = document.createElement('div');
1✔
1085
        setStyleProperties(divChildNode, { width: '100%', height: '100%' });
1✔
1086
        divChildNode.id = preRenderIds.child;
1✔
1087

1088
        if (typeof child === 'string') {
1!
1089
            divChildNode.innerHTML = child;
1✔
1090
        } else {
1091
            divChildNode.appendChild(child);
×
1092
        }
1093

1094
        return divChildNode;
1✔
1095
    }
1096

1097
    /**
1098
     * Creates the in-flow placeholder div inserted into the host element when
1099
     * showPreRender() is called. The wrapper observes this element to stay
1100
     * aligned with the host layout.
1101
     */
1102
    private createPreRenderPlaceholder(): HTMLDivElement {
1103
        const placeholder = document.createElement('div');
11✔
1104
        const id = this.getPreRenderIds();
11✔
1105
        const { width: frameWidth, height: frameHeight } = this.viewConfig.frameParams || {};
11✔
1106
        const width = getCssDimension(frameWidth || DEFAULT_EMBED_WIDTH);
11✔
1107
        const height = getCssDimension(frameHeight || DEFAULT_EMBED_HEIGHT);
11✔
1108
        placeholder.style.width = width;
11✔
1109
        placeholder.style.height = height;
11✔
1110
        // we can improve this , lol
1111
        placeholder.id = id.placeHolder;
11✔
1112
        return placeholder;
11✔
1113
    }
1114

1115
    protected insertIntoDOMForPreRender(child: string | Node): void {
1116
        const preRenderChild = this.createPreRenderChild(child);
21✔
1117
        const preRenderWrapper = this.createPreRenderWrapper();
21✔
1118
        preRenderWrapper.appendChild(preRenderChild);
21✔
1119

1120
        this.preRenderChild = preRenderChild;
21✔
1121
        this.preRenderWrapper = preRenderWrapper;
21✔
1122

1123
        if (preRenderChild instanceof HTMLIFrameElement) {
21✔
1124
            this.setIframeElement(preRenderChild);
20✔
1125
        }
1126

1127
        if (this.iFrame) {
21✔
1128
            this.iFrame.style.height = '100%';
20✔
1129
            this.iFrame.style.width = '100%';
20✔
1130
        }
1131

1132
        if (this.showPreRenderByDefault) {
21✔
1133
            this.showPreRender();
2✔
1134
        } else {
1135
            this.hidePreRender();
19✔
1136
        }
1137

1138
        document.body.appendChild(preRenderWrapper);
21✔
1139
    }
1140

1141
    private showPreRenderByDefault = false;
582✔
1142

1143
    protected insertIntoDOM(child: string | Node): void {
1144
        if (this.viewConfig.insertAsSibling) {
486✔
1145
            if (typeof child === 'string') {
12✔
1146
                const div = document.createElement('div');
1✔
1147
                div.innerHTML = child;
1✔
1148
                div.id = TS_EMBED_ID;
1✔
1149

1150
                child = div;
1✔
1151
            }
1152
            if (this.hostElement.nextElementSibling?.id === TS_EMBED_ID) {
12✔
1153
                this.hostElement.nextElementSibling.remove();
1✔
1154
            }
1155
            this.hostElement.parentElement.insertBefore(child, this.hostElement.nextSibling);
12✔
1156
            this.insertedDomEl = child;
12✔
1157
        } else if (typeof child === 'string') {
474✔
1158
            this.hostElement.innerHTML = child;
6✔
1159
            this.insertedDomEl = this.hostElement.children[0];
6✔
1160
        } else {
1161
            this.hostElement.innerHTML = '';
468✔
1162
            this.hostElement.appendChild(child);
468✔
1163
            this.insertedDomEl = child;
468✔
1164
        }
1165
    }
1166

1167
    /**
1168
     * Sets the height of the iframe
1169
     * @param height The height in pixels
1170
     */
1171
    protected setIFrameHeight(height: number | string): void {
1172
        if (this.isPreRendered) {
3!
NEW
1173
            if (this.insertedDomEl)
×
NEW
1174
                (this.insertedDomEl as HTMLElement).style.height = getCssDimension(height);
×
1175
            else 
NEW
1176
                this.preRenderWrapper.style.height = getCssDimension(height);
×
1177
        } else {
1178
            // normal (non-preRender) mode: size the iframe directly
1179
            this.iFrame.style.height = getCssDimension(height);
3✔
1180
        }
1181
    }
1182

1183
    /**
1184
     * We can process the customer given payload before sending it to the embed port
1185
     * Embed event handler -> responder -> createEmbedEventResponder -> send response
1186
     * @param eventPort The event port for a specific MessageChannel
1187
     * @param eventType The event type
1188
     * @returns 
1189
     */
1190
    protected createEmbedEventResponder = (eventPort: MessagePort | void, eventType: EmbedEvent) => {
582✔
1191

1192
        const getPayloadToSend = (payload: any) => {
93✔
1193
            if (eventType === EmbedEvent.OnBeforeGetVizDataIntercept) {
48✔
1194
                return processLegacyInterceptResponse(payload);
1✔
1195
            }
1196
            if (eventType === EmbedEvent.ApiIntercept) {
47!
1197
                return processApiInterceptResponse(payload);
×
1198
            }
1199
            return payload;
47✔
1200
        }
1201
        return (payload: any) => {
93✔
1202
            const payloadToSend = getPayloadToSend(payload);
48✔
1203
            this.triggerEventOnPort(eventPort, payloadToSend);
48✔
1204
        }
1205
    }
1206

1207
    private shouldSkipEvent(eventType: EmbedEvent, data: any): boolean {                                                                                                         
1208
        const errorType = data?.errorType ?? data?.data?.code;                                                                                                              
644!
1209
        if (                                                                                                                                                                     
644✔
1210
            eventType === EmbedEvent.Error
689✔
1211
            && errorType === EmbedErrorCodes.HOST_EVENT_VALIDATION                                                                                                                  
1212
            && (!getHostEventsConfig(this.viewConfig).useHostEventsV2 || getHostEventsConfig(this.viewConfig).shouldBypassPayloadValidation)                                                                                                                                
1213
        ) {
1214
            logger.warn(`Host Event Validation failed: ${data?.data?.message}`);
7!
1215
            return true;                                                                                                                                                         
7✔
1216
        }           
1217
        return false;
637✔
1218
    }                                                                                                                                                                            
1219
    /**
1220
     * Executes all registered event handlers for a particular event type
1221
     * @param eventType The event type
1222
     * @param data The payload invoked with the event handler
1223
     * @param eventPort The event Port for a specific MessageChannel
1224
     */
1225
    protected executeCallbacks(
1226
        eventType: EmbedEvent,
1227
        data: any,
1228
        eventPort?: MessagePort | void,
1229
    ): void {
1230
        if (this.shouldSkipEvent(eventType, data)) return;
644✔
1231
        const eventHandlers = this.eventHandlerMap.get(eventType) || [];
637✔
1232
        const allHandlers = this.eventHandlerMap.get(EmbedEvent.ALL) || [];
637✔
1233
        const callbacks = [...eventHandlers, ...allHandlers];
637✔
1234
        const dataStatus = data?.status || embedEventStatus.END;
637!
1235
        callbacks.forEach((callbackObj) => {
637✔
1236
            if (
93✔
1237
                // When start status is true it trigger only start releated
1238
                // payload
1239
                (callbackObj.options.start && dataStatus === embedEventStatus.START)
278✔
1240
                // When start status is false it trigger only end releated
1241
                // payload
1242
                || (!callbackObj.options.start && dataStatus === embedEventStatus.END)
1243
            ) {
1244
                const responder = this.createEmbedEventResponder(eventPort, eventType);
92✔
1245
                callbackObj.callback(data, responder);
92✔
1246
            }
1247
        });
1248
    }
1249

1250
    /**
1251
     * Returns the ThoughtSpot hostname or IP address.
1252
     */
1253
    protected getThoughtSpotHost(): string {
1254
        return this.thoughtSpotHost;
×
1255
    }
1256

1257
    /**
1258
     * Gets the v1 event type (if applicable) for the EmbedEvent type
1259
     * @param eventType The v2 event type
1260
     * @returns The corresponding v1 event type if one exists
1261
     * or else the v2 event type itself
1262
     */
1263
    protected getCompatibleEventType(eventType: EmbedEvent): EmbedEvent {
1264
        return V1EventMap[eventType] || eventType;
2,321✔
1265
    }
1266

1267
    /**
1268
     * Calculates the iframe center for the current visible viewPort
1269
     * of iframe using Scroll position of Host App, offsetTop for iframe
1270
     * in Host app. ViewPort height of the tab.
1271
     * @returns iframe Center in visible viewport,
1272
     *  Iframe height,
1273
     *  View port height.
1274
     */
1275
    protected getIframeCenter() {
1276
        const offsetTopClient = getOffsetTop(this.iFrame);
5✔
1277
        const scrollTopClient = window.scrollY;
5✔
1278
        const viewPortHeight = window.innerHeight;
5✔
1279
        const iframeHeight = this.iFrame.offsetHeight;
5✔
1280
        const iframeScrolled = scrollTopClient - offsetTopClient;
5✔
1281
        let iframeVisibleViewPort;
1282
        let iframeOffset;
1283

1284
        if (iframeScrolled < 0) {
5!
1285
            iframeVisibleViewPort = viewPortHeight - (offsetTopClient - scrollTopClient);
×
1286
            iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
×
1287
            iframeOffset = 0;
×
1288
        } else {
1289
            iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
5✔
1290
            iframeOffset = iframeScrolled;
5✔
1291
        }
1292
        const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
5✔
1293
        return {
5✔
1294
            iframeCenter,
1295
            iframeScrolled,
1296
            iframeHeight,
1297
            viewPortHeight,
1298
            iframeVisibleViewPort,
1299
        };
1300
    }
1301

1302
    /**
1303
     * Registers an event listener to trigger an alert when the ThoughtSpot app
1304
     * sends an event of a particular message type to the host application.
1305
     * @param messageType The message type
1306
     * @param callback A callback as a function
1307
     * @param options The message options
1308
     * @param isSelf
1309
     * @param isRegisteredBySDK
1310
     * @example
1311
     * ```js
1312
     * tsEmbed.on(EmbedEvent.Error, (data) => {
1313
     *   console.error(data);
1314
     * });
1315
     * ```
1316
     * @example
1317
     * ```js
1318
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1319
     *   console.log("Answer save clicked", data);
1320
     * }, {
1321
     *   start: true // This will trigger the callback on start of save
1322
     * });
1323
     * ```
1324
     */
1325
    public on(
1326
        messageType: EmbedEvent,
1327
        callback: MessageCallback,
1328
        options: MessageOptions = { start: false },
29✔
1329
        isRegisteredBySDK = false,
2,352✔
1330
    ): typeof TsEmbed.prototype {
1331
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_ON}-${messageType}`, {
3,642✔
1332
            isRegisteredBySDK,
1333
        });
1334
        if (this.isRendered) {
3,642✔
1335
            logger.warn('Please register event handlers before calling render');
2✔
1336
        }
1337
        const callbacks = this.eventHandlerMap.get(messageType) || [];
3,642✔
1338
        callbacks.push({ options, callback });
3,642✔
1339
        this.eventHandlerMap.set(messageType, callbacks);
3,642✔
1340
        return this;
3,642✔
1341
    }
1342

1343
    /**
1344
     * Removes an event listener for a particular event type.
1345
     * @param messageType The message type
1346
     * @param callback The callback to remove
1347
     * @example
1348
     * ```js
1349
     * const errorHandler = (data) => { console.error(data); };
1350
     * tsEmbed.on(EmbedEvent.Error, errorHandler);
1351
     * tsEmbed.off(EmbedEvent.Error, errorHandler);
1352
     * ```
1353
     */
1354
    public off(messageType: EmbedEvent, callback: MessageCallback): typeof TsEmbed.prototype {
1355
        const callbacks = this.eventHandlerMap.get(messageType) || [];
1!
1356
        const index = callbacks.findIndex((cb) => cb.callback === callback);
1✔
1357
        if (index > -1) {
1!
1358
            callbacks.splice(index, 1);
1✔
1359
        }
1360
        return this;
1✔
1361
    }
1362

1363
    /**
1364
     * Triggers an event on specific Port registered against
1365
     * for the EmbedEvent
1366
     * @param eventType The message type
1367
     * @param data The payload to send
1368
     * @param eventPort
1369
     * @param payload
1370
     */
1371
    private triggerEventOnPort(eventPort: MessagePort | void, payload: any) {
1372
        if (eventPort) {
48✔
1373
            try {
44✔
1374
                eventPort.postMessage({
44✔
1375
                    type: payload.type,
1376
                    data: payload.data,
1377
                });
1378
            } catch (e) {
1379
                eventPort.postMessage({ error: e });
×
1380
                logger.log(e);
×
1381
            }
1382
        } else {
1383
            logger.log('Event Port is not defined');
4✔
1384
        }
1385
    }
1386

1387
    /**
1388
    * @hidden
1389
    * Internal state to track if the embed container is loaded.
1390
    * This is used to trigger events after the embed container is loaded.
1391
    */
1392
    public isEmbedContainerLoaded = false;
582✔
1393

1394
    /**
1395
     * @hidden
1396
     * Internal state to track the callbacks to be executed after the embed container 
1397
     * is loaded.
1398
     * This is used to trigger events after the embed container is loaded.
1399
     */
1400
    private embedContainerReadyCallbacks: Array<() => void> = [];
582✔
1401

1402
    protected getPreRenderObj<T extends TsEmbed>(): T {
1403
        const embedObj = (this.preRenderWrapper as any)?.[this.embedNodeKey] as T;
46✔
1404
        if (embedObj === (this as any)) {
46✔
1405
            logger.info('embedObj is same as this');
7✔
1406
        }
1407
        return embedObj;
46✔
1408
    }
1409

1410
    private checkEmbedContainerLoaded() {
1411
        if (this.isEmbedContainerLoaded) return true;
49✔
1412

1413
        const preRenderObj = this.getPreRenderObj<TsEmbed>();
35✔
1414
        if (preRenderObj && preRenderObj.isEmbedContainerLoaded) {
35✔
1415
            this.isEmbedContainerLoaded = true;
6✔
1416
        }
1417

1418
        return this.isEmbedContainerLoaded;
35✔
1419
    }
1420
    private executeEmbedContainerReadyCallbacks() {
1421
        logger.debug('executePendingEvents', this.embedContainerReadyCallbacks);
15✔
1422
        this.embedContainerReadyCallbacks.forEach((callback) => {
15✔
1423
            callback?.();
16!
1424
        });
1425
        this.embedContainerReadyCallbacks = [];
15✔
1426
    }
1427

1428
    /**
1429
     * Executes a callback after the embed container is loaded.
1430
     * @param callback The callback to execute
1431
     */
1432
    protected executeAfterEmbedContainerLoaded(callback: () => void) {
1433
        if (this.checkEmbedContainerLoaded()) {
48✔
1434
            callback?.();
19!
1435
        } else {
1436
            logger.debug('pushing callback to embedContainerReadyCallbacks', callback);
29✔
1437
            this.embedContainerReadyCallbacks.push(callback);
29✔
1438
        }
1439
    }
1440

1441
    protected createEmbedContainerHandler = (source: EmbedEvent.AuthInit | EmbedEvent.EmbedListenerReady) => () => {
1,166✔
1442
        const processEmbedContainerReady = () => {
10✔
1443
            logger.debug('processEmbedContainerReady');
9✔
1444
            this.isEmbedContainerLoaded = true;
9✔
1445
            this.executeEmbedContainerReadyCallbacks();
9✔
1446
        }
1447
        if (source === EmbedEvent.AuthInit) {
10✔
1448
            const AUTH_INIT_FALLBACK_DELAY = 1000;
7✔
1449
            // Wait for 1 second to ensure the embed container is loaded
1450
            // This is a workaround to ensure the embed container is loaded
1451
            // this is needed until all clusters have EmbedListenerReady event
1452
            setTimeout(processEmbedContainerReady, AUTH_INIT_FALLBACK_DELAY);
7✔
1453
        } else if (source === EmbedEvent.EmbedListenerReady) {
3!
1454
            processEmbedContainerReady();
3✔
1455
        }
1456
    }
1457

1458
    /**
1459
     * Triggers an event to the embedded app
1460
     * @param {HostEvent} messageType The event type
1461
     * @param {any} data The payload to send with the message
1462
     * @param {ContextType} context Optional context type to specify the context from which the event is triggered.
1463
     * Use ContextType.Search for search answer context, ContextType.Answer for answer/explore context,
1464
     * ContextType.Liveboard for liveboard context, or ContextType.Spotter for spotter context.
1465
     * Available from SDK version 1.45.2 | ThoughtSpot: 26.3.0.cl
1466
     * @returns A promise that resolves with the response from the embedded app
1467
     * @example
1468
     * ```js
1469
     * // Trigger Pin event with context (SDK: 1.45.2+)
1470
     * import { HostEvent, ContextType } from '@thoughtspot/visual-embed-sdk';
1471
     * embed.trigger(HostEvent.Pin, {
1472
     *   vizId: "123",
1473
     *   liveboardId: "456"
1474
     * }, ContextType.Search);
1475
     * ```
1476
     * @version SDK: 1.45.2 | ThoughtSpot: 26.3.0.cl (for context parameter)
1477
     */
1478
    public async trigger<HostEventT extends HostEvent, PayloadT, ContextT extends ContextType>(
1479
        messageType: HostEventT,
1480
        data: TriggerPayload<PayloadT, HostEventT> = {} as any,
233✔
1481
        context?: ContextT,
1482
    ): Promise<TriggerResponse<PayloadT, HostEventT, ContextT>> {
1483
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`);
340✔
1484

1485
        if (!this.isRendered) {
340!
1486
            this.handleError({
×
1487
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1488
                message: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1489
                code: EmbedErrorCodes.RENDER_NOT_CALLED,
1490
                error: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1491
            });
1492
            return null;
×
1493
        }
1494

1495
        if (!messageType) {
340✔
1496
            this.handleError({
1✔
1497
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1498
                message: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1499
                code: EmbedErrorCodes.HOST_EVENT_TYPE_UNDEFINED,
1500
                error: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1501
            });
1502
            return null;
1✔
1503
        }
1504

1505
        // Check if iframe exists before triggering - 
1506
        // this prevents the error when auth fails
1507
        if (!this.iFrame) {
339✔
1508
            logger.debug(
22✔
1509
                `Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
1510
            );
1511
            return null;
22✔
1512
        }
1513

1514
        // send an empty object, this is needed for liveboard default handlers
1515
        return this.hostEventClient.triggerHostEvent(messageType, data, context).catch((err: Error & {
317✔
1516
            isValidationError?: boolean;
1517
            embedErrorDetails?: { errorType: ErrorDetailsTypes; message: string; code: EmbedErrorCodes; error: string };
1518
        }): Promise<null> => {
1519
            if (err?.isValidationError) {
×
1520
                const errorDetails = err.embedErrorDetails ?? {
×
1521
                    errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1522
                    message: err.message || ERROR_MESSAGE.UPDATEFILTERS_INVALID_PAYLOAD,
×
1523
                    code: EmbedErrorCodes.UPDATEFILTERS_INVALID_PAYLOAD,
1524
                    error: err.message,
1525
                };
1526
                this.handleError(errorDetails);
×
1527
            }
1528
            throw err;
×
1529
        });
1530
    }
1531

1532
    /**
1533
     * Triggers an event to the embedded app, skipping the UI flow.
1534
     * @param {UIPassthroughEvent} apiName - The name of the API to be triggered.
1535
     * @param {UIPassthroughRequest} parameters - The parameters to be passed to the API.
1536
     * @returns {Promise<UIPassthroughRequest>} - A promise that resolves with the response
1537
     * from the embedded app.
1538
     */
1539
    public async triggerUIPassThrough<UIPassthroughEventT extends UIPassthroughEvent>(
1540
        apiName: UIPassthroughEventT,
1541
        parameters: UIPassthroughRequest<UIPassthroughEventT>,
1542
    ): Promise<UIPassthroughArrayResponse<UIPassthroughEventT>> {
1543
        const response = this.hostEventClient.triggerUIPassthroughApi(apiName, parameters);
3✔
1544
        return response;
3✔
1545
    }
1546

1547
    /**
1548
     * Marks the ThoughtSpot object to have been rendered
1549
     * Needs to be overridden by subclasses to do the actual
1550
     * rendering of the iframe.
1551
     * @param args
1552
     */
1553
    public async render(): Promise<TsEmbed> {
1554
        if (!getIsInitCalled()) {
509✔
1555
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
2✔
1556
        }
1557
        if (this.shouldWaitForRenderPromise)
509✔
1558
            await this.isReadyForRenderPromise;
3✔
1559
        this.isRendered = true;
509✔
1560

1561
        return this;
509✔
1562
    }
1563

1564
    public getIframeSrc(): string {
1565
        return '';
1✔
1566
    }
1567

1568
    protected handleRenderForPrerender() {
1569
        return this.render();
15✔
1570
    }
1571

1572
    /**
1573
    * Context object for the embedded component.
1574
    * @returns {ContextObject} The current context object containing the page type and object ids.
1575
    * @example
1576
    * ```js
1577
    * const context = await embed.getCurrentContext();
1578
    * console.log(context);
1579
    * 
1580
    * // Example output
1581
    * {
1582
    *   stack: [
1583
    *     {
1584
    *       name: 'Liveboard',
1585
    *       type: ContextType.Liveboard,
1586
    *       objectIds: {
1587
    *         liveboardId: '123',
1588
    *       },
1589
    *     },
1590
    *   ],
1591
    *   currentContext: {
1592
    *     name: 'Liveboard',
1593
    *     type: ContextType.Liveboard,
1594
    *     objectIds: {
1595
    *       liveboardId: '123',
1596
    *     },
1597
    *   },
1598
    * }
1599
    * ```
1600
     * @version SDK: 1.45.2 | ThoughtSpot: 26.3.0.cl
1601
     */
1602
    public async getCurrentContext(): Promise<ContextObject> {
1603
        return new Promise((resolve) => {
2✔
1604
            this.executeAfterEmbedContainerLoaded(async () => {
2✔
1605
                const context = await this.trigger(HostEvent.GetPageContext, {});
2✔
1606
                resolve(context);
2✔
1607
            });
1608
        });
1609
    }
1610

1611
    /**
1612
     * Generates the event name for a "Subscribed" embed event.
1613
     *
1614
     * This helper appends the "Subscribed" suffix to a given host or action event,
1615
     * allowing you to listen for subscription lifecycle events in a consistent format.
1616
     *
1617
     * @param eventName - The host or action event to generate the subscribed event name for.
1618
     * @returns The formatted event name (e.g., "Save Subscribed").
1619
     * 
1620
     * @version SDK: 1.47.2 | ThoughtSpot: 26.3.0.cl
1621
     */
1622
    public subscribedEvent(eventName: HostEvent | Action): string {
1623
        return `${eventName} ${EmbedEvent.Subscribed}`;
×
1624
    }
1625

1626
    /**
1627
     * Creates the preRender shell
1628
     * @param showPreRenderByDefault - Show the preRender after render, hidden by default
1629
     */
1630

1631
    public async preRender(showPreRenderByDefault = false, replaceExistingPreRender = false): Promise<TsEmbed> {
43✔
1632
        if (!this.viewConfig.preRenderId) {
29✔
1633
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1✔
1634
            return this;
1✔
1635
        }
1636
        this.isPreRendered = true;
28✔
1637
        this.showPreRenderByDefault = showPreRenderByDefault;
28✔
1638

1639

1640
        const isAlreadyRendered = this.connectPreRendered();
28✔
1641
        if (isAlreadyRendered && !replaceExistingPreRender) {
28✔
1642
            if (this.showPreRenderByDefault) {
8✔
1643
                this.showPreRender();
7✔
1644
            }
1645
            return this;
8✔
1646
        }
1647

1648
        return this.handleRenderForPrerender();
20✔
1649
    }
1650

1651
    /**
1652
     * Get the Post Url Params for THOUGHTSPOT from the current
1653
     * host app URL.
1654
     * THOUGHTSPOT URL params starts with a prefix "ts-"
1655
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
1656
     */
1657
    public getThoughtSpotPostUrlParams(
1658
        additionalParams: { [key: string]: string | number } = {},
299✔
1659
    ): string {
1660
        const urlHash = window.location.hash;
518✔
1661
        const queryParams = window.location.search;
518✔
1662
        const postHashParams = urlHash.split('?');
518✔
1663
        const postURLParams = postHashParams[postHashParams.length - 1];
518✔
1664
        const queryParamsObj = new URLSearchParams(queryParams);
518✔
1665
        const postURLParamsObj = new URLSearchParams(postURLParams);
518✔
1666
        const params = new URLSearchParams();
518✔
1667

1668
        const addKeyValuePairCb = (value: string, key: string): void => {
518✔
1669
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
8✔
1670
                params.append(key, value);
5✔
1671
            }
1672
        };
1673
        queryParamsObj.forEach(addKeyValuePairCb);
518✔
1674
        postURLParamsObj.forEach(addKeyValuePairCb);
518✔
1675
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
518✔
1676

1677
        let tsParams = params.toString();
518✔
1678
        tsParams = tsParams ? `?${tsParams}` : '';
518✔
1679

1680
        return tsParams;
518✔
1681
    }
1682

1683
    /**
1684
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
1685
     * @version SDK: 1.19.1 | ThoughtSpot: *
1686
     */
1687
    public destroy(): void {
1688
        try {
35✔
1689
            this.removeFullscreenChangeHandler();
35✔
1690
            this.unsubscribeToEvents();
35✔
1691
            this.preRenderWrapper?.remove();
35✔
1692
            if (!this.isRendered) {
35✔
1693
                return;
1✔
1694
            }
1695
            if (!getEmbedConfig().waitForCleanupOnDestroy) {
34✔
1696
                this.trigger(HostEvent.DestroyEmbed)
30✔
1697
                this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
30!
1698
            } else {
1699
                const cleanupTimeout = getEmbedConfig().cleanupTimeout;
4✔
1700
                Promise.race([
4✔
1701
                    this.trigger(HostEvent.DestroyEmbed),
1702
                    new Promise((resolve) => setTimeout(resolve, cleanupTimeout)),
4✔
1703
                ]).catch((e) => {
1704
                    logger.log('Error destroying TS Embed', e);
1✔
1705
                }).finally(() => {
1706
                    try {
4✔
1707
                        this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
4!
1708
                    } catch (e) {
1709
                        logger.log('Error removing DOM element on destroy', e);
×
1710
                    }
1711
                });
1712
            }
1713
        } catch (e) {
1714
            logger.log('Error destroying TS Embed', e);
1✔
1715
        }
1716
    }
1717

1718
    public getUnderlyingFrameElement(): HTMLIFrameElement {
1719
        return this.iFrame;
1✔
1720
    }
1721

1722
    /**
1723
     * Prerenders a generic instance of the TS component.
1724
     * This means without the path but with the flags already applied.
1725
     * This is useful for prerendering the component in the background.
1726
     * @version SDK: 1.22.0
1727
     * @returns
1728
     */
1729
    public async prerenderGeneric(): Promise<any> {
1730
        if (!getIsInitCalled()) {
8✔
1731
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
1✔
1732
        }
1733
        if (this.shouldWaitForRenderPromise)
8✔
1734
            await this.isReadyForRenderPromise;
1✔
1735

1736
        const prerenderFrameSrc = this.getRootIframeSrc();
8✔
1737
        this.isRendered = true;
8✔
1738
        return this.renderIFrame(prerenderFrameSrc);
8✔
1739
    }
1740

1741
    protected beforePrerenderVisible(): void {
1742
        // We can ignore this as its a bit expensive and the newer customers 
1743
        // have moved on to UpdateEmbedParams supported clusters
1744
        // this.validatePreRenderViewConfig(this.viewConfig);
1745
        logger.debug('triggering UpdateEmbedParams', this.viewConfig);
17✔
1746
        this.executeAfterEmbedContainerLoaded(async () => {
17✔
1747
            try {
12✔
1748
                const params = await this.getUpdateEmbedParamsObject();
12✔
1749
                this.trigger(HostEvent.UpdateEmbedParams, params);
11✔
1750
            } catch (error) {
1751
                logger.error(ERROR_MESSAGE.UPDATE_PARAMS_FAILED, error);
1✔
1752
                this.handleError({
1✔
1753
                    errorType: ErrorDetailsTypes.API,
1754
                    message: error?.message || ERROR_MESSAGE.UPDATE_PARAMS_FAILED,
4!
1755
                    code: EmbedErrorCodes.UPDATE_PARAMS_FAILED,
1756
                    error: error?.message || error,
4!
1757
                });
1758
            }
1759
        });
1760
    }
1761

1762
    private validatePreRenderViewConfig = (viewConfig: ViewConfig) => {
582✔
UNCOV
1763
        const preRenderAllowedKeys = ['preRenderId', 'vizId', 'liveboardId'];
×
NEW
1764
        const preRenderedObject = (this.preRenderWrapper as any)?.[this.embedNodeKey] as TsEmbed;
×
UNCOV
1765
        if (!preRenderedObject) return;
×
UNCOV
1766
        if (viewConfig.preRenderId) {
×
UNCOV
1767
            const allOtherKeys = Object.keys(viewConfig).filter(
×
UNCOV
1768
                (key) => !preRenderAllowedKeys.includes(key) && !key.startsWith('on'),
×
1769
            );
1770

UNCOV
1771
            allOtherKeys.forEach((key: keyof ViewConfig) => {
×
UNCOV
1772
                if (
×
1773
                    !isUndefined(viewConfig[key])
×
1774
                    && !isEqual(viewConfig[key], preRenderedObject.viewConfig[key])
1775
                ) {
UNCOV
1776
                    logger.warn(
×
1777
                        `${viewConfig.embedComponentType || 'Component'} was pre-rendered with `
×
1778
                        + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
1779
                        + `but a different value "${JSON.stringify(viewConfig[key])}" `
1780
                        + 'was passed to the Embed component. '
1781
                        + 'The new value provided is ignored, the value provided during '
1782
                        + 'preRender is used.',
1783
                    );
1784
                }
1785
            });
1786
        }
1787
    };
1788

1789
    /**
1790
     * Displays the pre-rendered component inside the host element.
1791
     * If the component has not been pre-rendered yet, it initiates rendering first.
1792
     * Inserts a placeholder element into the host and positions the pre-render
1793
     * wrapper to overlay it.
1794
     */
1795
    public async showPreRender(): Promise<TsEmbed> {
1796
        if (this.shouldWaitForRenderPromise)
21!
NEW
1797
            await this.isReadyForRenderPromise;
×
1798

1799
        if (!this.viewConfig.preRenderId) {
21✔
1800
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1✔
1801
            return this;
1✔
1802
        }
1803
        if (!this.isPreRenderConnected()) {
20✔
1804
            // this will call showPreRender down the line
1805
            return this.preRender(true);
9✔
1806
        }
1807
        this.isRendered = true;
11✔
1808
        this.beforePrerenderVisible();
11✔
1809

1810
        if (this.hostElement) {
11!
1811
            this.insertedDomEl = this.createPreRenderPlaceholder();
11✔
1812
            if ((this.viewConfig as { fullHeight: boolean }).fullHeight) {
11!
1813
                // If fullHeight has already sized the wrapper, seed the placeholder
1814
                // with the same height so syncPreRenderStyle gets an accurate rect.
NEW
1815
                const existingHeight = this.preRenderWrapper.style.height;
×
NEW
1816
                if (existingHeight) {
×
NEW
1817
                    (this.insertedDomEl as HTMLDivElement).style.height = existingHeight;
×
1818
                }
1819
            }
1820

1821
            const placeHolderId = this.getPreRenderIds().placeHolder;
11✔
1822
            const oldEle = this.hostElement.querySelector(`#${placeHolderId}`);
11✔
1823
            if (oldEle) {
11!
NEW
1824
                this.hostElement.removeChild(oldEle);
×
1825
            }
1826

1827
            this.hostElement.appendChild(this.insertedDomEl);
11✔
1828

1829
            this.syncPreRenderStyle();
11✔
1830

1831
            if (!this.viewConfig.doNotTrackPreRenderSize) {
11!
1832
                const observeTarget = (this.insertedDomEl as HTMLElement) ?? this.hostElement;
11!
1833
                this.resizeObserver = new ResizeObserver((entries) => {
11✔
1834
                    entries.forEach((entry) => {
1✔
1835
                        if (entry.contentRect && entry.target === observeTarget) {
1!
1836
                            setStyleProperties(this.preRenderWrapper, {
1✔
1837
                                width: `${entry.contentRect.width}px`,
1838
                                height: `${entry.contentRect.height}px`,
1839
                            });
1840
                        }
1841
                    });
1842
                });
1843
                this.resizeObserver.observe(observeTarget);
11✔
1844
            }
1845
        }
1846

1847
        removeStyleProperties(this.preRenderWrapper, ['z-index', 'opacity', 'pointer-events', 'overflow']);
11✔
1848
        this.subscribeToEvents();
11✔
1849
        
1850
        // Setup fullscreen change handler for prerendered components
1851
        if (this.iFrame) {
11!
1852
            this.setupFullscreenChangeHandler();
11✔
1853
        }
1854

1855
        return this;
11✔
1856
    }
1857

1858
    protected getPreRenderPlaceHolderElement() {
1859
        return this.insertedDomEl as HTMLDivElement;
43✔
1860
    }
1861

1862
    /**
1863
     * Synchronizes the style properties of the PreRender component with the embedding
1864
     * element. This function adjusts the position, width, and height of the PreRender
1865
     * component
1866
     * to match the dimensions and position of the embedding element.
1867
     * @throws {Error} Throws an error if the embedding element (passed as domSelector)
1868
     * is not defined or not found.
1869
     */
1870
    public syncPreRenderStyle(): void {
1871
        if (!this.isPreRenderConnected() || !this.getPreRenderPlaceHolderElement()) {
12✔
1872
            logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
1✔
1873
            return;
1✔
1874
        }
1875
        const elBoundingClient = this.getPreRenderPlaceHolderElement().getBoundingClientRect();
11✔
1876

1877
        setStyleProperties(this.preRenderWrapper, {
11✔
1878
            top: `${elBoundingClient.y + window.scrollY}px`,
1879
            left: `${elBoundingClient.x + window.scrollX}px`,
1880
            width: `${elBoundingClient.width}px`,
1881
            height: `${elBoundingClient.height}px`,
1882
            position: 'absolute'
1883
        });
1884
    }
1885

1886
    /**
1887
     * Hides the PreRender component if it is available.
1888
     * If the component is not preRendered, it issues a warning.
1889
     */
1890
    public hidePreRender(): void {
1891
        logger.debug('HidePreRender Called');
22✔
1892
        if (!this.isPreRenderConnected()) {
22✔
1893
            // if the embed component is not preRendered , nothing to hide
1894
            logger.warn('PreRender should be called before hiding it using hidePreRender.');
1✔
1895
            return;
1✔
1896
        }
1897
        const preRenderHideStyles = {
21✔
1898
            opacity: '0',
1899
            pointerEvents: 'none',
1900
            zIndex: '-1000',
1901
            position: 'absolute',
1902
            top: '0',
1903
            left: '0',
1904
            overflow: 'hidden',
1905
        };
1906
        setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
21✔
1907

1908
        if (this.resizeObserver) {
21✔
1909
            this.resizeObserver.disconnect();
2✔
1910
        }
1911

1912
        const placeHolderEle = this.getPreRenderPlaceHolderElement();
21✔
1913
        if (placeHolderEle) {
21✔
1914
            placeHolderEle.parentElement.removeChild(placeHolderEle);
2✔
1915
        }
1916

1917
        this.unsubscribeToEvents();
21✔
1918
    }
1919

1920
    /**
1921
     * Retrieves unique HTML element IDs for PreRender-related elements.
1922
     * These IDs are constructed based on the provided 'preRenderId' from 'viewConfig'.
1923
     * @returns {object} An object containing the IDs for the PreRender elements.
1924
     * @property {string} wrapper - The HTML element ID for the PreRender wrapper.
1925
     * @property {string} child - The HTML element ID for the PreRender child.
1926
     */
1927
    public getPreRenderIds() {
1928
        return {
103✔
1929
            wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
1930
            child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1931
            placeHolder: `tsEmbed-pre-render-placeholder-${this.viewConfig.preRenderId}`,
1932
        };
1933
    }
1934

1935
    /**
1936
     * Returns the answerService which can be used to make arbitrary graphql calls on top
1937
     * session.
1938
     * @param vizId [Optional] to get for a specific viz in case of a Liveboard.
1939
     * @version SDK: 1.25.0 | ThoughtSpot: 9.10.0
1940
     */
1941
    public async getAnswerService(vizId?: string): Promise<AnswerService> {
1942
        const { session } = await this.trigger(HostEvent.GetAnswerSession, vizId ? { vizId } : {});
1!
1943
        return new AnswerService(session, null, this.embedConfig.thoughtSpotHost);
1✔
1944
    }
1945

1946
    /**
1947
     * Set up fullscreen change detection to automatically trigger ExitPresentMode
1948
     * when user exits fullscreen mode
1949
     */
1950
    private setupFullscreenChangeHandler() {
1951
        const embedConfig = getEmbedConfig();
32✔
1952
        const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
32!
1953

1954
        if (disableFullscreenPresentation) {
32✔
1955
            return;
29✔
1956
        }
1957

1958
        if (this.fullscreenChangeHandler) {
3!
1959
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
×
1960
        }
1961

1962
        this.fullscreenChangeHandler = () => {
3✔
1963
            const isFullscreen = !!document.fullscreenElement;
5✔
1964
            if (!isFullscreen) {
5✔
1965
                logger.info('Exited fullscreen mode - triggering ExitPresentMode');
2✔
1966
                // Only trigger if iframe is available and contentWindow is
1967
                // accessible
1968
                if (this.iFrame && this.iFrame.contentWindow) {
2✔
1969
                    this.trigger(HostEvent.ExitPresentMode);
1✔
1970
                } else {
1971
                    logger.debug('Skipping ExitPresentMode - iframe contentWindow not available');
1✔
1972
                }
1973
            }
1974
        };
1975

1976
        document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
3✔
1977
    }
1978

1979
    /**
1980
     * Remove fullscreen change handler
1981
     */
1982
    private removeFullscreenChangeHandler() {
1983
        if (this.fullscreenChangeHandler) {
36!
1984
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
×
1985
            this.fullscreenChangeHandler = null;
×
1986
        }
1987
    }
1988
}
1989

1990
/**
1991
 * Base class for embedding v1 experience
1992
 * Note: The v1 version of ThoughtSpot Blink works on the AngularJS stack
1993
 * which is currently under migration to v2
1994
 * @inheritdoc
1995
 */
1996
export class V1Embed extends TsEmbed {
14✔
1997
    protected viewConfig: ViewConfig;
1998

1999
    constructor(domSelector: DOMSelector, viewConfig: ViewConfig) {
2000
        super(domSelector, viewConfig);
367✔
2001
        this.viewConfig = {
367✔
2002
            excludeRuntimeFiltersfromURL: true,
2003
            excludeRuntimeParametersfromURL: true,
2004
            ...viewConfig,
2005
        };
2006
    }
2007

2008
    /**
2009
     * Render the app in an iframe and set up event handlers
2010
     * @param iframeSrc
2011
     */
2012
    protected renderV1Embed(iframeSrc: string): Promise<any> {
2013
        return this.renderIFrame(iframeSrc);
336✔
2014
    }
2015

2016
    protected getRootIframeSrc(): string {
2017
        const queryParams = this.getEmbedParams();
345✔
2018
        let queryString = queryParams;
345✔
2019

2020
        if (!this.viewConfig.excludeRuntimeParametersfromURL) {
345✔
2021
            const runtimeParameters = this.viewConfig.runtimeParameters;
1✔
2022
            const parameterQuery = getRuntimeParameters(runtimeParameters || []);
1!
2023
            queryString = [parameterQuery, queryParams].filter(Boolean).join('&');
1✔
2024
        }
2025

2026
        if (!this.viewConfig.excludeRuntimeFiltersfromURL) {
345✔
2027
            const runtimeFilters = this.viewConfig.runtimeFilters;
8✔
2028

2029
            const filterQuery = getFilterQuery(runtimeFilters || []);
8!
2030
            queryString = [filterQuery, queryString].filter(Boolean).join('&');
8✔
2031
        }
2032
        return this.viewConfig.enableV2Shell_experimental
345✔
2033
            ? this.getEmbedBasePath(queryString)
2034
            : this.getV1EmbedBasePath(queryString);
2035
    }
2036

2037
    /**
2038
     * @inheritdoc
2039
     * @example
2040
     * ```js
2041
     * tsEmbed.on(EmbedEvent.Error, (data) => {
2042
     *   console.error(data);
2043
     * });
2044
     * ```
2045
     * @example
2046
     * ```js
2047
     * tsEmbed.on(EmbedEvent.Save, (data) => {
2048
     *   console.log("Answer save clicked", data);
2049
     * }, {
2050
     *   start: true // This will trigger the callback on start of save
2051
     * });
2052
     * ```
2053
     */
2054
    public on(
2055
        messageType: EmbedEvent,
2056
        callback: MessageCallback,
2057
        options: MessageOptions = { start: false },
161✔
2058
    ): typeof TsEmbed.prototype {
2059
        const eventType = this.getCompatibleEventType(messageType);
2,321✔
2060
        return super.on(eventType, callback, options);
2,321✔
2061
    }
2062

2063
    /**
2064
     * Only for testing purposes.
2065
     * @hidden
2066
     */
2067

2068
    public test__executeCallbacks = this.executeCallbacks;
367✔
2069
}
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