@thoughtspot/visual-embed-sdk 1.42.0 → 1.42.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/cjs/package.json +2 -2
  2. package/cjs/src/api-intercept.d.ts +27 -0
  3. package/cjs/src/api-intercept.d.ts.map +1 -0
  4. package/cjs/src/api-intercept.js +115 -0
  5. package/cjs/src/api-intercept.js.map +1 -0
  6. package/cjs/src/api-intercept.spec.d.ts +2 -0
  7. package/cjs/src/api-intercept.spec.d.ts.map +1 -0
  8. package/cjs/src/api-intercept.spec.js +122 -0
  9. package/cjs/src/api-intercept.spec.js.map +1 -0
  10. package/cjs/src/embed/app.d.ts.map +1 -1
  11. package/cjs/src/embed/app.js +7 -2
  12. package/cjs/src/embed/app.js.map +1 -1
  13. package/cjs/src/embed/app.spec.js +20 -0
  14. package/cjs/src/embed/app.spec.js.map +1 -1
  15. package/cjs/src/embed/hostEventClient/contracts.d.ts +11 -1
  16. package/cjs/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  17. package/cjs/src/embed/hostEventClient/contracts.js +1 -0
  18. package/cjs/src/embed/hostEventClient/contracts.js.map +1 -1
  19. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  20. package/cjs/src/embed/liveboard.js +4 -1
  21. package/cjs/src/embed/liveboard.js.map +1 -1
  22. package/cjs/src/embed/liveboard.spec.js +22 -0
  23. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  24. package/cjs/src/embed/search.d.ts.map +1 -1
  25. package/cjs/src/embed/search.js +3 -1
  26. package/cjs/src/embed/search.js.map +1 -1
  27. package/cjs/src/embed/ts-embed.d.ts +15 -0
  28. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  29. package/cjs/src/embed/ts-embed.js +94 -26
  30. package/cjs/src/embed/ts-embed.js.map +1 -1
  31. package/cjs/src/embed/ts-embed.spec.d.ts.map +1 -1
  32. package/cjs/src/embed/ts-embed.spec.js +168 -0
  33. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  34. package/cjs/src/errors.d.ts +1 -0
  35. package/cjs/src/errors.d.ts.map +1 -1
  36. package/cjs/src/errors.js +1 -0
  37. package/cjs/src/errors.js.map +1 -1
  38. package/cjs/src/index.d.ts +2 -2
  39. package/cjs/src/index.d.ts.map +1 -1
  40. package/cjs/src/index.js +2 -1
  41. package/cjs/src/index.js.map +1 -1
  42. package/cjs/src/react/all-types-export.d.ts +1 -1
  43. package/cjs/src/react/all-types-export.d.ts.map +1 -1
  44. package/cjs/src/react/all-types-export.js +2 -1
  45. package/cjs/src/react/all-types-export.js.map +1 -1
  46. package/cjs/src/types.d.ts +100 -4
  47. package/cjs/src/types.d.ts.map +1 -1
  48. package/cjs/src/types.js +39 -1
  49. package/cjs/src/types.js.map +1 -1
  50. package/cjs/src/utils/processData.d.ts +1 -1
  51. package/cjs/src/utils/processData.d.ts.map +1 -1
  52. package/cjs/src/utils/processData.js +8 -8
  53. package/cjs/src/utils/processData.js.map +1 -1
  54. package/dist/index-BCC3Z072.js +7371 -0
  55. package/dist/index-BEzW4MDA.js +7371 -0
  56. package/dist/{index-BpSohedu.js → index-DvNA626T.js} +1 -1
  57. package/dist/src/api-intercept.d.ts +27 -0
  58. package/dist/src/api-intercept.d.ts.map +1 -0
  59. package/dist/src/api-intercept.spec.d.ts +2 -0
  60. package/dist/src/api-intercept.spec.d.ts.map +1 -0
  61. package/dist/src/embed/app.d.ts.map +1 -1
  62. package/dist/src/embed/hostEventClient/contracts.d.ts +11 -1
  63. package/dist/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  64. package/dist/src/embed/liveboard.d.ts.map +1 -1
  65. package/dist/src/embed/search.d.ts.map +1 -1
  66. package/dist/src/embed/ts-embed.d.ts +15 -0
  67. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  68. package/dist/src/embed/ts-embed.spec.d.ts.map +1 -1
  69. package/dist/src/errors.d.ts +1 -0
  70. package/dist/src/errors.d.ts.map +1 -1
  71. package/dist/src/index.d.ts +2 -2
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/react/all-types-export.d.ts +1 -1
  74. package/dist/src/react/all-types-export.d.ts.map +1 -1
  75. package/dist/src/types.d.ts +100 -4
  76. package/dist/src/types.d.ts.map +1 -1
  77. package/dist/src/utils/processData.d.ts +1 -1
  78. package/dist/src/utils/processData.d.ts.map +1 -1
  79. package/dist/tsembed-react.es.js +266 -51
  80. package/dist/tsembed-react.js +265 -50
  81. package/dist/tsembed.es.js +267 -52
  82. package/dist/tsembed.js +265 -50
  83. package/dist/visual-embed-sdk-react-full.d.ts +124 -4
  84. package/dist/visual-embed-sdk-react.d.ts +121 -4
  85. package/dist/visual-embed-sdk.d.ts +124 -4
  86. package/lib/package.json +2 -2
  87. package/lib/src/api-intercept.d.ts +27 -0
  88. package/lib/src/api-intercept.d.ts.map +1 -0
  89. package/lib/src/api-intercept.js +108 -0
  90. package/lib/src/api-intercept.js.map +1 -0
  91. package/lib/src/api-intercept.spec.d.ts +2 -0
  92. package/lib/src/api-intercept.spec.d.ts.map +1 -0
  93. package/lib/src/api-intercept.spec.js +119 -0
  94. package/lib/src/api-intercept.spec.js.map +1 -0
  95. package/lib/src/embed/app.d.ts.map +1 -1
  96. package/lib/src/embed/app.js +7 -2
  97. package/lib/src/embed/app.js.map +1 -1
  98. package/lib/src/embed/app.spec.js +20 -0
  99. package/lib/src/embed/app.spec.js.map +1 -1
  100. package/lib/src/embed/hostEventClient/contracts.d.ts +11 -1
  101. package/lib/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  102. package/lib/src/embed/hostEventClient/contracts.js +1 -0
  103. package/lib/src/embed/hostEventClient/contracts.js.map +1 -1
  104. package/lib/src/embed/liveboard.d.ts.map +1 -1
  105. package/lib/src/embed/liveboard.js +4 -1
  106. package/lib/src/embed/liveboard.js.map +1 -1
  107. package/lib/src/embed/liveboard.spec.js +22 -0
  108. package/lib/src/embed/liveboard.spec.js.map +1 -1
  109. package/lib/src/embed/search.d.ts.map +1 -1
  110. package/lib/src/embed/search.js +3 -1
  111. package/lib/src/embed/search.js.map +1 -1
  112. package/lib/src/embed/ts-embed.d.ts +15 -0
  113. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  114. package/lib/src/embed/ts-embed.js +94 -26
  115. package/lib/src/embed/ts-embed.js.map +1 -1
  116. package/lib/src/embed/ts-embed.spec.d.ts.map +1 -1
  117. package/lib/src/embed/ts-embed.spec.js +168 -0
  118. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  119. package/lib/src/errors.d.ts +1 -0
  120. package/lib/src/errors.d.ts.map +1 -1
  121. package/lib/src/errors.js +1 -0
  122. package/lib/src/errors.js.map +1 -1
  123. package/lib/src/index.d.ts +2 -2
  124. package/lib/src/index.d.ts.map +1 -1
  125. package/lib/src/index.js +2 -2
  126. package/lib/src/index.js.map +1 -1
  127. package/lib/src/react/all-types-export.d.ts +1 -1
  128. package/lib/src/react/all-types-export.d.ts.map +1 -1
  129. package/lib/src/react/all-types-export.js +1 -1
  130. package/lib/src/react/all-types-export.js.map +1 -1
  131. package/lib/src/types.d.ts +100 -4
  132. package/lib/src/types.d.ts.map +1 -1
  133. package/lib/src/types.js +38 -0
  134. package/lib/src/types.js.map +1 -1
  135. package/lib/src/utils/processData.d.ts +1 -1
  136. package/lib/src/utils/processData.d.ts.map +1 -1
  137. package/lib/src/utils/processData.js +8 -8
  138. package/lib/src/utils/processData.js.map +1 -1
  139. package/package.json +2 -2
  140. package/src/api-intercept.spec.ts +147 -0
  141. package/src/api-intercept.ts +134 -0
  142. package/src/embed/app.spec.ts +28 -0
  143. package/src/embed/app.ts +9 -1
  144. package/src/embed/hostEventClient/contracts.ts +10 -0
  145. package/src/embed/liveboard.spec.ts +30 -0
  146. package/src/embed/liveboard.ts +5 -0
  147. package/src/embed/search.ts +3 -1
  148. package/src/embed/ts-embed.spec.ts +221 -5
  149. package/src/embed/ts-embed.ts +129 -43
  150. package/src/errors.ts +1 -0
  151. package/src/index.ts +2 -0
  152. package/src/react/all-types-export.ts +1 -0
  153. package/src/types.ts +102 -3
  154. package/src/utils/processData.ts +11 -11
@@ -61,6 +61,14 @@ import { UIPassthroughEvent } from './hostEventClient/contracts';
61
61
  import * as sessionInfoService from '../utils/sessionInfoService';
62
62
  import * as authToken from '../authToken';
63
63
 
64
+ // Mock api-intercept by default to avoid altering existing APP_INIT payload
65
+ // expectations
66
+ jest.mock('../api-intercept', () => ({
67
+ getInterceptInitData: jest.fn(() => ({})),
68
+ handleInterceptEvent: jest.fn(),
69
+ processLegacyInterceptResponse: jest.fn((x: any) => x),
70
+ }));
71
+
64
72
  jest.mock('../utils/processTrigger');
65
73
 
66
74
  const mockProcessTrigger = processTrigger as jest.Mock;
@@ -1045,7 +1053,7 @@ describe('Unit test case for ts embed', () => {
1045
1053
  type: EmbedEvent.APP_INIT,
1046
1054
  data: {},
1047
1055
  };
1048
-
1056
+
1049
1057
  // Create a SearchEmbed with valid custom actions to test
1050
1058
  // CustomActionsValidationResult
1051
1059
  const searchEmbed = new SearchEmbed(getRootEl(), {
@@ -1067,7 +1075,7 @@ describe('Unit test case for ts embed', () => {
1067
1075
  }
1068
1076
  ]
1069
1077
  });
1070
-
1078
+
1071
1079
  searchEmbed.render();
1072
1080
  const mockPort: any = {
1073
1081
  postMessage: jest.fn(),
@@ -1116,7 +1124,7 @@ describe('Unit test case for ts embed', () => {
1116
1124
  customVariablesForThirdPartyTools: {},
1117
1125
  },
1118
1126
  });
1119
-
1127
+
1120
1128
  // Verify that CustomActionsValidationResult structure is
1121
1129
  // correct
1122
1130
  const appInitData = mockPort.postMessage.mock.calls[0][0].data;
@@ -1137,7 +1145,7 @@ describe('Unit test case for ts embed', () => {
1137
1145
  })
1138
1146
  ])
1139
1147
  );
1140
-
1148
+
1141
1149
  // Verify actions are sorted by name (alphabetically)
1142
1150
  expect(appInitData.customActions[0].name).toBe('Another Valid Action');
1143
1151
  expect(appInitData.customActions[1].name).toBe('Valid Action');
@@ -2488,7 +2496,7 @@ describe('Unit test case for ts embed', () => {
2488
2496
  });
2489
2497
 
2490
2498
  afterAll((): void => {
2491
- window.location = location as any;
2499
+ (window.location as any) = location;
2492
2500
  });
2493
2501
 
2494
2502
  it('get url params for TS', () => {
@@ -3345,4 +3353,212 @@ describe('Unit test case for ts embed', () => {
3345
3353
  expect(searchEmbed.isEmbedContainerLoaded).toBe(true);
3346
3354
  });
3347
3355
  });
3356
+
3357
+ describe('Online event listener registration after auth failure', () => {
3358
+ beforeAll(() => {
3359
+ init({
3360
+ thoughtSpotHost: 'tshost',
3361
+ authType: AuthType.None,
3362
+ loginFailedMessage: 'Not logged in',
3363
+ });
3364
+ });
3365
+
3366
+ test('should register online event listener when authentication fails', async () => {
3367
+ const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
3368
+ jest.spyOn(baseInstance, 'getAuthPromise').mockRejectedValueOnce(
3369
+ new Error('Auth failed'),
3370
+ );
3371
+ const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
3372
+ addEventListenerSpy.mockClear();
3373
+ await searchEmbed.render();
3374
+ await executeAfterWait(() => {
3375
+ expect(getRootEl().innerHTML).toContain('Not logged in');
3376
+ const onlineListenerCalls = addEventListenerSpy.mock.calls.filter(
3377
+ (call) => call[0] === 'online',
3378
+ );
3379
+ expect(onlineListenerCalls).toHaveLength(1);
3380
+ const offlineListenerCalls = addEventListenerSpy.mock.calls.filter(
3381
+ (call) => call[0] === 'offline',
3382
+ );
3383
+ expect(offlineListenerCalls).toHaveLength(1);
3384
+ const messageListenerCalls = addEventListenerSpy.mock.calls.filter(
3385
+ (call) => call[0] === 'message',
3386
+ );
3387
+ expect(messageListenerCalls).toHaveLength(0);
3388
+ });
3389
+
3390
+ addEventListenerSpy.mockRestore();
3391
+ });
3392
+
3393
+ test('should attempt to trigger reload when online event occurs after auth failure', async () => {
3394
+ jest.spyOn(baseInstance, 'getAuthPromise').mockRejectedValueOnce(
3395
+ new Error('Auth failed'),
3396
+ );
3397
+ const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
3398
+ const triggerSpy = jest.spyOn(searchEmbed, 'trigger').mockResolvedValue(null);
3399
+ await searchEmbed.render();
3400
+
3401
+ await executeAfterWait(() => {
3402
+ expect(getRootEl().innerHTML).toContain('Not logged in');
3403
+ triggerSpy.mockClear();
3404
+ const onlineEvent = new Event('online');
3405
+ window.dispatchEvent(onlineEvent);
3406
+ expect(triggerSpy).toHaveBeenCalledWith(HostEvent.Reload);
3407
+ });
3408
+
3409
+ triggerSpy.mockReset();
3410
+ });
3411
+
3412
+ test('should handle online event gracefully when no iframe exists', async () => {
3413
+ jest.spyOn(baseInstance, 'getAuthPromise').mockRejectedValueOnce(
3414
+ new Error('Auth failed'),
3415
+ );
3416
+ const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
3417
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
3418
+ await searchEmbed.render();
3419
+ await executeAfterWait(() => {
3420
+ expect(getRootEl().innerHTML).toContain('Not logged in');
3421
+ const onlineEvent = new Event('online');
3422
+ expect(() => {
3423
+ window.dispatchEvent(onlineEvent);
3424
+ }).not.toThrow();
3425
+ });
3426
+
3427
+ errorSpy.mockReset();
3428
+ });
3429
+
3430
+ test('should register all event listeners when authentication succeeds', async () => {
3431
+ const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
3432
+ jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(true);
3433
+ const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
3434
+ addEventListenerSpy.mockClear();
3435
+ await searchEmbed.render();
3436
+ await executeAfterWait(() => {
3437
+ const onlineListenerCalls = addEventListenerSpy.mock.calls.filter(
3438
+ (call) => call[0] === 'online',
3439
+ );
3440
+ expect(onlineListenerCalls).toHaveLength(1);
3441
+ const offlineListenerCalls = addEventListenerSpy.mock.calls.filter(
3442
+ (call) => call[0] === 'offline',
3443
+ );
3444
+ expect(offlineListenerCalls).toHaveLength(1);
3445
+ const messageListenerCalls = addEventListenerSpy.mock.calls.filter(
3446
+ (call) => call[0] === 'message',
3447
+ );
3448
+ expect(messageListenerCalls).toHaveLength(1);
3449
+ });
3450
+
3451
+ addEventListenerSpy.mockRestore();
3452
+ });
3453
+ test('should successfully trigger reload when online event occurs after auth success', async () => {
3454
+ jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(true);
3455
+ const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
3456
+ const triggerSpy = jest.spyOn(searchEmbed, 'trigger').mockResolvedValue({} as any);
3457
+ await searchEmbed.render();
3458
+ await executeAfterWait(() => {
3459
+ triggerSpy.mockClear();
3460
+ const onlineEvent = new Event('online');
3461
+ window.dispatchEvent(onlineEvent);
3462
+ expect(triggerSpy).toHaveBeenCalledWith(HostEvent.Reload);
3463
+ });
3464
+ triggerSpy.mockReset();
3465
+ });
3466
+ });
3467
+ });
3468
+
3469
+ describe('API Intercept integration in TsEmbed', () => {
3470
+ beforeEach(() => {
3471
+ document.body.innerHTML = getDocumentBody();
3472
+ jest.clearAllMocks();
3473
+ });
3474
+
3475
+ test('APP_INIT includes API intercept init data when enabled', async () => {
3476
+ init({
3477
+ thoughtSpotHost: 'tshost',
3478
+ authType: AuthType.None,
3479
+ enableApiIntercept: true,
3480
+ interceptTimeout: 1234,
3481
+ } as any);
3482
+
3483
+ // Include intercept setup via viewConfig too
3484
+ const { InterceptedApiType } = require('../types');
3485
+
3486
+ // Override mocked getInterceptInitData for this test only
3487
+ const apiInterceptModule = require('../api-intercept');
3488
+ apiInterceptModule.getInterceptInitData.mockReturnValue({
3489
+ enableApiIntercept: true,
3490
+ interceptTimeout: 1234,
3491
+ interceptUrls: [
3492
+ 'http://tshost/prism/?op=CreateAnswerSession',
3493
+ 'http://tshost/prism/?op=GetV2SourceDetail',
3494
+ 'http://tshost/custom/path',
3495
+ ],
3496
+ });
3497
+ const searchEmbed = new SearchEmbed(getRootEl(), {
3498
+ ...defaultViewConfig,
3499
+ enableApiIntercept: true,
3500
+ interceptUrls: [InterceptedApiType.METADATA, '/custom/path'],
3501
+ } as any);
3502
+
3503
+ searchEmbed.render();
3504
+
3505
+ const mockPort: any = { postMessage: jest.fn() };
3506
+ const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {} };
3507
+
3508
+ await executeAfterWait(() => {
3509
+ const iframe = getIFrameEl();
3510
+ postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort);
3511
+ });
3512
+
3513
+ await executeAfterWait(() => {
3514
+ expect(mockPort.postMessage).toHaveBeenCalledWith(
3515
+ expect.objectContaining({
3516
+ type: EmbedEvent.APP_INIT,
3517
+ data: expect.objectContaining({
3518
+ enableApiIntercept: true,
3519
+ interceptTimeout: 1234,
3520
+ interceptUrls: expect.arrayContaining([
3521
+ expect.stringContaining('CreateAnswerSession'),
3522
+ expect.stringContaining('GetV2SourceDetail'),
3523
+ expect.stringContaining('custom/path'),
3524
+ ]),
3525
+ }),
3526
+ }),
3527
+ );
3528
+ });
3529
+ });
3530
+
3531
+ test('ApiIntercept event delegates to handleInterceptEvent and bypasses default callbacks', async () => {
3532
+ init({ thoughtSpotHost: 'tshost', authType: AuthType.None } as any);
3533
+
3534
+ const apiInterceptModule = require('../api-intercept');
3535
+ const handleInterceptSpy = jest.spyOn(apiInterceptModule, 'handleInterceptEvent').mockResolvedValue(undefined);
3536
+
3537
+ const searchEmbed = new SearchEmbed(getRootEl(), {
3538
+ ...defaultViewConfig,
3539
+ enableApiIntercept: true,
3540
+ } as any);
3541
+
3542
+ const apiCb = jest.fn();
3543
+ searchEmbed.on(EmbedEvent.ApiIntercept as any, apiCb as any);
3544
+
3545
+ searchEmbed.render();
3546
+
3547
+ const mockPort: any = { postMessage: jest.fn() };
3548
+ const eventPayload = {
3549
+ type: EmbedEvent.ApiIntercept,
3550
+ data: JSON.stringify({ input: '/prism/?op=GetChartWithData', init: { body: '{}' } }),
3551
+ };
3552
+
3553
+ await executeAfterWait(() => {
3554
+ const iframe = getIFrameEl();
3555
+ postMessageToParent(iframe.contentWindow, eventPayload, mockPort);
3556
+ });
3557
+
3558
+ await executeAfterWait(() => {
3559
+ expect(handleInterceptSpy).toHaveBeenCalled();
3560
+ expect(apiCb).not.toHaveBeenCalled();
3561
+ });
3562
+ });
3348
3563
  });
3564
+
@@ -71,6 +71,7 @@ import { getEmbedConfig } from './embedConfig';
71
71
  import { ERROR_MESSAGE } from '../errors';
72
72
  import { getPreauthInfo } from '../utils/sessionInfoService';
73
73
  import { HostEventClient } from './hostEventClient/host-event-client';
74
+ import { getInterceptInitData, handleInterceptEvent, processLegacyInterceptResponse } from '../api-intercept';
74
75
 
75
76
  const { version } = pkgInfo;
76
77
 
@@ -201,7 +202,7 @@ export class TsEmbed {
201
202
  });
202
203
  const embedConfig = getEmbedConfig();
203
204
  this.embedConfig = embedConfig;
204
-
205
+
205
206
  this.hostEventClient = new HostEventClient(this.iFrame);
206
207
  this.isReadyForRenderPromise = getInitPromise().then(async () => {
207
208
  if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
@@ -312,31 +313,11 @@ export class TsEmbed {
312
313
  private subscribedListeners: Record<string, any> = {};
313
314
 
314
315
  /**
315
- * Adds a global event listener to window for "message" events.
316
- * ThoughtSpot detects if a particular event is targeted to this
317
- * embed instance through an identifier contained in the payload,
318
- * and executes the registered callbacks accordingly.
316
+ * Subscribe to network events (online/offline) that should
317
+ * work regardless of auth status
319
318
  */
320
- private subscribeToEvents() {
321
- this.unsubscribeToEvents();
322
- const messageEventListener = (event: MessageEvent<any>) => {
323
- const eventType = this.getEventType(event);
324
- const eventPort = this.getEventPort(event);
325
- const eventData = this.formatEventData(event, eventType);
326
- if (event.source === this.iFrame.contentWindow) {
327
- this.executeCallbacks(
328
- eventType,
329
- processEventData(
330
- eventType,
331
- eventData,
332
- this.thoughtSpotHost,
333
- this.isPreRendered ? this.preRenderWrapper : this.el,
334
- ),
335
- eventPort,
336
- );
337
- }
338
- };
339
- window.addEventListener('message', messageEventListener);
319
+ private subscribeToNetworkEvents() {
320
+ this.unsubscribeToNetworkEvents();
340
321
 
341
322
  const onlineEventListener = (e: Event) => {
342
323
  this.trigger(HostEvent.Reload);
@@ -344,7 +325,7 @@ export class TsEmbed {
344
325
  window.addEventListener('online', onlineEventListener);
345
326
 
346
327
  const offlineEventListener = (e: Event) => {
347
- const offlineWarning = 'Network not Detected. Embed is offline. Please reconnect and refresh';
328
+ const offlineWarning = ERROR_MESSAGE.OFFLINE_WARNING;
348
329
  this.executeCallbacks(EmbedEvent.Error, {
349
330
  offlineWarning,
350
331
  });
@@ -352,11 +333,84 @@ export class TsEmbed {
352
333
  };
353
334
  window.addEventListener('offline', offlineEventListener);
354
335
 
355
- this.subscribedListeners = {
356
- message: messageEventListener,
357
- online: onlineEventListener,
358
- offline: offlineEventListener,
359
- };
336
+ this.subscribedListeners.online = onlineEventListener;
337
+ this.subscribedListeners.offline = offlineEventListener;
338
+ }
339
+
340
+ private messageEventListener = async (event: MessageEvent<any>) => {
341
+ const eventType = this.getEventType(event);
342
+ const eventPort = this.getEventPort(event);
343
+ const eventData = this.formatEventData(event, eventType);
344
+ if (event.source === this.iFrame.contentWindow) {
345
+ const processedEventData = await processEventData(
346
+ eventType,
347
+ eventData,
348
+ this.thoughtSpotHost,
349
+ this.isPreRendered ? this.preRenderWrapper : this.el,
350
+ );
351
+
352
+ const executeEvent = (_eventType: EmbedEvent, data: any) => {
353
+ this.executeCallbacks(_eventType, data, eventPort);
354
+ }
355
+
356
+ if (eventType === EmbedEvent.ApiIntercept && this.viewConfig.enableApiIntercept) {
357
+ const getUnsavedAnswerTml = async (props: { sessionId?: string, vizId?: string }) => {
358
+ const response = await this.triggerUIPassThrough(UIPassthroughEvent.GetUnsavedAnswerTML, props);
359
+ return response[0]?.value;
360
+ }
361
+ handleInterceptEvent({ eventData: processedEventData, executeEvent, embedConfig: this.embedConfig, viewConfig: this.viewConfig, getUnsavedAnswerTml });
362
+ return;
363
+ }
364
+
365
+ this.executeCallbacks(
366
+ eventType,
367
+ processedEventData,
368
+ eventPort,
369
+ );
370
+ }
371
+ };
372
+ /**
373
+ * Subscribe to message events that depend on successful iframe setup
374
+ */
375
+ private subscribeToMessageEvents() {
376
+ this.unsubscribeToMessageEvents();
377
+
378
+ window.addEventListener('message', this.messageEventListener);
379
+
380
+ this.subscribedListeners.message = this.messageEventListener;
381
+ }
382
+
383
+
384
+ /**
385
+ * Adds event listeners for both network and message events.
386
+ * This maintains backward compatibility with the existing method.
387
+ * Adds a global event listener to window for "message" events.
388
+ * ThoughtSpot detects if a particular event is targeted to this
389
+ * embed instance through an identifier contained in the payload,
390
+ * and executes the registered callbacks accordingly.
391
+ */
392
+ private subscribeToEvents() {
393
+ this.subscribeToNetworkEvents();
394
+ this.subscribeToMessageEvents();
395
+ }
396
+
397
+
398
+ private unsubscribeToNetworkEvents() {
399
+ if (this.subscribedListeners.online) {
400
+ window.removeEventListener('online', this.subscribedListeners.online);
401
+ delete this.subscribedListeners.online;
402
+ }
403
+ if (this.subscribedListeners.offline) {
404
+ window.removeEventListener('offline', this.subscribedListeners.offline);
405
+ delete this.subscribedListeners.offline;
406
+ }
407
+ }
408
+
409
+ private unsubscribeToMessageEvents() {
410
+ if (this.subscribedListeners.message) {
411
+ window.removeEventListener('message', this.subscribedListeners.message);
412
+ delete this.subscribedListeners.message;
413
+ }
360
414
  }
361
415
 
362
416
  private unsubscribeToEvents() {
@@ -391,7 +445,7 @@ export class TsEmbed {
391
445
  message: customActionsResult.errors,
392
446
  });
393
447
  }
394
- return {
448
+ const baseInitData = {
395
449
  customisations: getCustomisations(this.embedConfig, this.viewConfig),
396
450
  authToken,
397
451
  runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL
@@ -410,7 +464,10 @@ export class TsEmbed {
410
464
  this.embedConfig.customVariablesForThirdPartyTools || {},
411
465
  hiddenListColumns: this.viewConfig.hiddenListColumns || [],
412
466
  customActions: customActionsResult.actions,
467
+ ...getInterceptInitData(this.embedConfig, this.viewConfig),
413
468
  };
469
+
470
+ return baseInitData;
414
471
  }
415
472
 
416
473
  protected async getAppInitData() {
@@ -798,6 +855,9 @@ export class TsEmbed {
798
855
 
799
856
  uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
800
857
 
858
+ // Always subscribe to network events, regardless of auth status
859
+ this.subscribeToNetworkEvents();
860
+
801
861
  return getAuthPromise()
802
862
  ?.then((isLoggedIn: boolean) => {
803
863
  if (!isLoggedIn) {
@@ -843,7 +903,9 @@ export class TsEmbed {
843
903
  el.remove();
844
904
  });
845
905
  }
846
- this.subscribeToEvents();
906
+ // Subscribe to message events only after successful
907
+ // auth and iframe setup
908
+ this.subscribeToMessageEvents();
847
909
  })
848
910
  .catch((error) => {
849
911
  nextInQueue();
@@ -983,6 +1045,21 @@ export class TsEmbed {
983
1045
  this.iFrame.style.height = getCssDimension(height);
984
1046
  }
985
1047
 
1048
+ protected createEmbedEventResponder = (eventPort: MessagePort | void, eventType: EmbedEvent) => {
1049
+
1050
+ const { enableApiIntercept } = getInterceptInitData(this.embedConfig, this.viewConfig);
1051
+ if (eventType === EmbedEvent.OnBeforeGetVizDataIntercept && enableApiIntercept) {
1052
+ return (payload: any) => {
1053
+ const payloadToSend = processLegacyInterceptResponse(payload);
1054
+ this.triggerEventOnPort(eventPort, payloadToSend);
1055
+ }
1056
+ }
1057
+
1058
+ return (payload: any) => {
1059
+ this.triggerEventOnPort(eventPort, payload);
1060
+ }
1061
+ }
1062
+
986
1063
  /**
987
1064
  * Executes all registered event handlers for a particular event type
988
1065
  * @param eventType The event type
@@ -1007,9 +1084,8 @@ export class TsEmbed {
1007
1084
  // payload
1008
1085
  || (!callbackObj.options.start && dataStatus === embedEventStatus.END)
1009
1086
  ) {
1010
- callbackObj.callback(data, (payload) => {
1011
- this.triggerEventOnPort(eventPort, payload);
1012
- });
1087
+ const responder = this.createEmbedEventResponder(eventPort, eventType);
1088
+ callbackObj.callback(data, responder);
1013
1089
  }
1014
1090
  });
1015
1091
  }
@@ -1151,12 +1227,12 @@ export class TsEmbed {
1151
1227
  }
1152
1228
  }
1153
1229
 
1154
- /**
1155
- * @hidden
1156
- * Internal state to track if the embed container is loaded.
1157
- * This is used to trigger events after the embed container is loaded.
1158
- */
1159
- public isEmbedContainerLoaded = false;
1230
+ /**
1231
+ * @hidden
1232
+ * Internal state to track if the embed container is loaded.
1233
+ * This is used to trigger events after the embed container is loaded.
1234
+ */
1235
+ public isEmbedContainerLoaded = false;
1160
1236
 
1161
1237
  /**
1162
1238
  * @hidden
@@ -1243,6 +1319,16 @@ export class TsEmbed {
1243
1319
  this.handleError('Host event type is undefined');
1244
1320
  return null;
1245
1321
  }
1322
+
1323
+ // Check if iframe exists before triggering -
1324
+ // this prevents the error when auth fails
1325
+ if (!this.iFrame) {
1326
+ logger.debug(
1327
+ `Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
1328
+ );
1329
+ return null;
1330
+ }
1331
+
1246
1332
  // send an empty object, this is needed for liveboard default handlers
1247
1333
  return this.hostEventClient.triggerHostEvent(messageType, data);
1248
1334
  }
@@ -1298,7 +1384,7 @@ export class TsEmbed {
1298
1384
  }
1299
1385
  this.isPreRendered = true;
1300
1386
  this.showPreRenderByDefault = showPreRenderByDefault;
1301
-
1387
+
1302
1388
  const isAlreadyRendered = this.connectPreRendered();
1303
1389
  if (isAlreadyRendered && !replaceExistingPreRender) {
1304
1390
  return this;
package/src/errors.ts CHANGED
@@ -19,6 +19,7 @@ export const ERROR_MESSAGE = {
19
19
  MISSING_REPORTING_OBSERVER: 'ReportingObserver not supported',
20
20
  RENDER_CALLED_BEFORE_INIT: 'Looks like render was called before calling init, the render won\'t start until init is called.\nFor more info check\n1. https://developers.thoughtspot.com/docs/Function_init#_init\n2.https://developers.thoughtspot.com/docs/getting-started#initSdk',
21
21
  SPOTTER_AGENT_NOT_INITIALIZED: 'SpotterAgent not initialized',
22
+ OFFLINE_WARNING : 'Network not Detected. Embed is offline. Please reconnect and refresh',
22
23
  };
23
24
 
24
25
  export const CUSTOM_ACTIONS_ERROR_MESSAGE = {
package/src/index.ts CHANGED
@@ -64,6 +64,7 @@ import {
64
64
  ListPageColumns,
65
65
  CustomActionsPosition,
66
66
  CustomActionTarget,
67
+ InterceptedApiType,
67
68
  } from './types';
68
69
  import { CustomCssVariables } from './css-variables';
69
70
  import { SageEmbed, SageViewConfig } from './embed/sage';
@@ -152,6 +153,7 @@ export {
152
153
  DataPanelCustomColumnGroupsAccordionState,
153
154
  CustomActionsPosition,
154
155
  CustomActionTarget,
156
+ InterceptedApiType,
155
157
  };
156
158
 
157
159
  export { resetCachedAuthToken } from './authToken';
@@ -59,4 +59,5 @@ export {
59
59
  resetCachedAuthToken,
60
60
  UIPassthroughEvent,
61
61
  DataPanelCustomColumnGroupsAccordionState,
62
+ InterceptedApiType,
62
63
  } from '../index';