@thoughtspot/visual-embed-sdk 1.47.2 → 1.48.0

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 (190) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/auth.d.ts.map +1 -1
  3. package/cjs/src/auth.js +11 -1
  4. package/cjs/src/auth.js.map +1 -1
  5. package/cjs/src/auth.spec.js +38 -0
  6. package/cjs/src/auth.spec.js.map +1 -1
  7. package/cjs/src/authToken.d.ts +2 -0
  8. package/cjs/src/authToken.d.ts.map +1 -1
  9. package/cjs/src/authToken.js +7 -5
  10. package/cjs/src/authToken.js.map +1 -1
  11. package/cjs/src/embed/app.d.ts +7 -2
  12. package/cjs/src/embed/app.d.ts.map +1 -1
  13. package/cjs/src/embed/app.js +4 -1
  14. package/cjs/src/embed/app.js.map +1 -1
  15. package/cjs/src/embed/app.spec.js +121 -0
  16. package/cjs/src/embed/app.spec.js.map +1 -1
  17. package/cjs/src/embed/conversation.d.ts +2 -1
  18. package/cjs/src/embed/conversation.d.ts.map +1 -1
  19. package/cjs/src/embed/conversation.js.map +1 -1
  20. package/cjs/src/embed/liveboard.d.ts +1 -1
  21. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  22. package/cjs/src/embed/liveboard.js +4 -1
  23. package/cjs/src/embed/liveboard.js.map +1 -1
  24. package/cjs/src/embed/liveboard.spec.js +32 -0
  25. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  26. package/cjs/src/embed/search.d.ts +24 -1
  27. package/cjs/src/embed/search.d.ts.map +1 -1
  28. package/cjs/src/embed/search.js +15 -2
  29. package/cjs/src/embed/search.js.map +1 -1
  30. package/cjs/src/embed/search.spec.js +99 -0
  31. package/cjs/src/embed/search.spec.js.map +1 -1
  32. package/cjs/src/embed/spotter-utils.d.ts +3 -0
  33. package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
  34. package/cjs/src/embed/spotter-utils.js +11 -3
  35. package/cjs/src/embed/spotter-utils.js.map +1 -1
  36. package/cjs/src/embed/spotter-utils.spec.js +51 -0
  37. package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
  38. package/cjs/src/embed/ts-embed.d.ts +1 -0
  39. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  40. package/cjs/src/embed/ts-embed.js +17 -5
  41. package/cjs/src/embed/ts-embed.js.map +1 -1
  42. package/cjs/src/embed/ts-embed.spec.js +168 -0
  43. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  44. package/cjs/src/index.d.ts +2 -2
  45. package/cjs/src/index.d.ts.map +1 -1
  46. package/cjs/src/index.js +8 -1
  47. package/cjs/src/index.js.map +1 -1
  48. package/cjs/src/mixpanel-service.d.ts.map +1 -1
  49. package/cjs/src/mixpanel-service.js +2 -0
  50. package/cjs/src/mixpanel-service.js.map +1 -1
  51. package/cjs/src/mixpanel-service.spec.js +2 -0
  52. package/cjs/src/mixpanel-service.spec.js.map +1 -1
  53. package/cjs/src/test/test-utils.d.ts +1 -0
  54. package/cjs/src/test/test-utils.d.ts.map +1 -1
  55. package/cjs/src/test/test-utils.js +26 -1
  56. package/cjs/src/test/test-utils.js.map +1 -1
  57. package/cjs/src/tokenizedFetch.d.ts.map +1 -1
  58. package/cjs/src/tokenizedFetch.js +12 -9
  59. package/cjs/src/tokenizedFetch.js.map +1 -1
  60. package/cjs/src/tokenizedFetch.spec.d.ts +2 -0
  61. package/cjs/src/tokenizedFetch.spec.d.ts.map +1 -0
  62. package/cjs/src/tokenizedFetch.spec.js +68 -0
  63. package/cjs/src/tokenizedFetch.spec.js.map +1 -0
  64. package/cjs/src/types.d.ts +466 -13
  65. package/cjs/src/types.d.ts.map +1 -1
  66. package/cjs/src/types.js +149 -5
  67. package/cjs/src/types.js.map +1 -1
  68. package/dist/{index-CFZ7RDZ9.js → index-Ck-r09gt.js} +1 -1
  69. package/dist/src/auth.d.ts.map +1 -1
  70. package/dist/src/authToken.d.ts +2 -0
  71. package/dist/src/authToken.d.ts.map +1 -1
  72. package/dist/src/embed/app.d.ts +7 -2
  73. package/dist/src/embed/app.d.ts.map +1 -1
  74. package/dist/src/embed/conversation.d.ts +2 -1
  75. package/dist/src/embed/conversation.d.ts.map +1 -1
  76. package/dist/src/embed/liveboard.d.ts +1 -1
  77. package/dist/src/embed/liveboard.d.ts.map +1 -1
  78. package/dist/src/embed/search.d.ts +24 -1
  79. package/dist/src/embed/search.d.ts.map +1 -1
  80. package/dist/src/embed/spotter-utils.d.ts +3 -0
  81. package/dist/src/embed/spotter-utils.d.ts.map +1 -1
  82. package/dist/src/embed/ts-embed.d.ts +1 -0
  83. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  84. package/dist/src/index.d.ts +2 -2
  85. package/dist/src/index.d.ts.map +1 -1
  86. package/dist/src/mixpanel-service.d.ts.map +1 -1
  87. package/dist/src/test/test-utils.d.ts +1 -0
  88. package/dist/src/test/test-utils.d.ts.map +1 -1
  89. package/dist/src/tokenizedFetch.d.ts.map +1 -1
  90. package/dist/src/tokenizedFetch.spec.d.ts +2 -0
  91. package/dist/src/tokenizedFetch.spec.d.ts.map +1 -0
  92. package/dist/src/types.d.ts +466 -13
  93. package/dist/src/types.d.ts.map +1 -1
  94. package/dist/tsembed-react.es.js +226 -29
  95. package/dist/tsembed-react.js +225 -28
  96. package/dist/tsembed.es.js +227 -30
  97. package/dist/tsembed.js +225 -28
  98. package/dist/visual-embed-sdk-react-full.d.ts +469 -15
  99. package/dist/visual-embed-sdk-react.d.ts +469 -15
  100. package/dist/visual-embed-sdk.d.ts +497 -15
  101. package/lib/package.json +1 -1
  102. package/lib/src/auth.d.ts.map +1 -1
  103. package/lib/src/auth.js +12 -2
  104. package/lib/src/auth.js.map +1 -1
  105. package/lib/src/auth.spec.js +38 -0
  106. package/lib/src/auth.spec.js.map +1 -1
  107. package/lib/src/authToken.d.ts +2 -0
  108. package/lib/src/authToken.d.ts.map +1 -1
  109. package/lib/src/authToken.js +2 -2
  110. package/lib/src/authToken.js.map +1 -1
  111. package/lib/src/embed/app.d.ts +7 -2
  112. package/lib/src/embed/app.d.ts.map +1 -1
  113. package/lib/src/embed/app.js +4 -1
  114. package/lib/src/embed/app.js.map +1 -1
  115. package/lib/src/embed/app.spec.js +122 -1
  116. package/lib/src/embed/app.spec.js.map +1 -1
  117. package/lib/src/embed/conversation.d.ts +2 -1
  118. package/lib/src/embed/conversation.d.ts.map +1 -1
  119. package/lib/src/embed/conversation.js.map +1 -1
  120. package/lib/src/embed/liveboard.d.ts +1 -1
  121. package/lib/src/embed/liveboard.d.ts.map +1 -1
  122. package/lib/src/embed/liveboard.js +4 -1
  123. package/lib/src/embed/liveboard.js.map +1 -1
  124. package/lib/src/embed/liveboard.spec.js +32 -0
  125. package/lib/src/embed/liveboard.spec.js.map +1 -1
  126. package/lib/src/embed/search.d.ts +24 -1
  127. package/lib/src/embed/search.d.ts.map +1 -1
  128. package/lib/src/embed/search.js +15 -2
  129. package/lib/src/embed/search.js.map +1 -1
  130. package/lib/src/embed/search.spec.js +100 -1
  131. package/lib/src/embed/search.spec.js.map +1 -1
  132. package/lib/src/embed/spotter-utils.d.ts +3 -0
  133. package/lib/src/embed/spotter-utils.d.ts.map +1 -1
  134. package/lib/src/embed/spotter-utils.js +11 -3
  135. package/lib/src/embed/spotter-utils.js.map +1 -1
  136. package/lib/src/embed/spotter-utils.spec.js +51 -0
  137. package/lib/src/embed/spotter-utils.spec.js.map +1 -1
  138. package/lib/src/embed/ts-embed.d.ts +1 -0
  139. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  140. package/lib/src/embed/ts-embed.js +16 -4
  141. package/lib/src/embed/ts-embed.js.map +1 -1
  142. package/lib/src/embed/ts-embed.spec.js +168 -0
  143. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  144. package/lib/src/index.d.ts +2 -2
  145. package/lib/src/index.d.ts.map +1 -1
  146. package/lib/src/index.js +2 -2
  147. package/lib/src/index.js.map +1 -1
  148. package/lib/src/mixpanel-service.d.ts.map +1 -1
  149. package/lib/src/mixpanel-service.js +2 -0
  150. package/lib/src/mixpanel-service.js.map +1 -1
  151. package/lib/src/mixpanel-service.spec.js +2 -0
  152. package/lib/src/mixpanel-service.spec.js.map +1 -1
  153. package/lib/src/test/test-utils.d.ts +1 -0
  154. package/lib/src/test/test-utils.d.ts.map +1 -1
  155. package/lib/src/test/test-utils.js +25 -1
  156. package/lib/src/test/test-utils.js.map +1 -1
  157. package/lib/src/tokenizedFetch.d.ts.map +1 -1
  158. package/lib/src/tokenizedFetch.js +13 -10
  159. package/lib/src/tokenizedFetch.js.map +1 -1
  160. package/lib/src/tokenizedFetch.spec.d.ts +2 -0
  161. package/lib/src/tokenizedFetch.spec.d.ts.map +1 -0
  162. package/lib/src/tokenizedFetch.spec.js +65 -0
  163. package/lib/src/tokenizedFetch.spec.js.map +1 -0
  164. package/lib/src/types.d.ts +466 -13
  165. package/lib/src/types.d.ts.map +1 -1
  166. package/lib/src/types.js +148 -4
  167. package/lib/src/types.js.map +1 -1
  168. package/lib/src/visual-embed-sdk.d.ts +497 -15
  169. package/package.json +1 -1
  170. package/src/auth.spec.ts +55 -1
  171. package/src/auth.ts +11 -2
  172. package/src/authToken.ts +2 -2
  173. package/src/embed/app.spec.ts +154 -0
  174. package/src/embed/app.ts +13 -2
  175. package/src/embed/conversation.ts +2 -1
  176. package/src/embed/liveboard.spec.ts +46 -0
  177. package/src/embed/liveboard.ts +9 -4
  178. package/src/embed/search.spec.ts +118 -0
  179. package/src/embed/search.ts +43 -1
  180. package/src/embed/spotter-utils.spec.ts +52 -0
  181. package/src/embed/spotter-utils.ts +19 -3
  182. package/src/embed/ts-embed.spec.ts +220 -0
  183. package/src/embed/ts-embed.ts +15 -4
  184. package/src/index.ts +16 -0
  185. package/src/mixpanel-service.spec.ts +2 -0
  186. package/src/mixpanel-service.ts +2 -0
  187. package/src/test/test-utils.ts +33 -1
  188. package/src/tokenizedFetch.spec.ts +81 -0
  189. package/src/tokenizedFetch.ts +14 -11
  190. package/src/types.ts +496 -13
@@ -2,6 +2,7 @@ import { DefaultAppInitData, ErrorDetailsTypes, EmbedErrorCodes } from '../types
2
2
  import { validateHttpUrl } from '../utils';
3
3
  import { ERROR_MESSAGE } from '../errors';
4
4
  import type { SpotterSidebarViewConfig } from './conversation';
5
+ import type { VisualizationOverrides } from '../types';
5
6
 
6
7
  /**
7
8
  * Resolves enablePastConversationsSidebar with
@@ -22,10 +23,16 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
22
23
  viewConfig: {
23
24
  spotterSidebarConfig?: SpotterSidebarViewConfig;
24
25
  enablePastConversationsSidebar?: boolean;
26
+ visualOverrides?: VisualizationOverrides;
25
27
  },
26
28
  handleError: (err: any) => void,
27
- ): T & { embedParams?: { spotterSidebarConfig?: SpotterSidebarViewConfig } } {
28
- const { spotterSidebarConfig, enablePastConversationsSidebar } = viewConfig;
29
+ ): T & {
30
+ embedParams?: {
31
+ spotterSidebarConfig?: SpotterSidebarViewConfig;
32
+ visualOverridesParams?: VisualizationOverrides | null;
33
+ };
34
+ } {
35
+ const { spotterSidebarConfig, enablePastConversationsSidebar, visualOverrides } = viewConfig;
29
36
 
30
37
  const resolvedEnablePastConversations = resolveEnablePastConversationsSidebar({
31
38
  spotterSidebarConfigValue: spotterSidebarConfig?.enablePastConversationsSidebar,
@@ -33,7 +40,15 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
33
40
  });
34
41
 
35
42
  const hasConfig = spotterSidebarConfig || resolvedEnablePastConversations !== undefined;
36
- if (!hasConfig) return defaultAppInitData;
43
+ if (!hasConfig) {
44
+ if (visualOverrides === undefined) {
45
+ return defaultAppInitData;
46
+ }
47
+ return {
48
+ ...defaultAppInitData,
49
+ embedParams: { visualOverridesParams: visualOverrides },
50
+ };
51
+ }
37
52
 
38
53
  const resolvedSidebarConfig: SpotterSidebarViewConfig = {
39
54
  ...spotterSidebarConfig,
@@ -60,6 +75,7 @@ export function buildSpotterSidebarAppInitData<T extends DefaultAppInitData>(
60
75
  embedParams: {
61
76
  ...((defaultAppInitData as any).embedParams || {}),
62
77
  spotterSidebarConfig: resolvedSidebarConfig,
78
+ ...(visualOverrides !== undefined ? { visualOverridesParams: visualOverrides } : {}),
63
79
  },
64
80
  };
65
81
  }
@@ -97,6 +97,7 @@ beforeAll(() => {
97
97
  const customisations = {
98
98
  style: {
99
99
  customCSS: {},
100
+ customCSSUrl: undefined as string | undefined,
100
101
  },
101
102
  content: {},
102
103
  };
@@ -132,6 +133,9 @@ const getMockAppInitPayload = (data: any) => {
132
133
  customVariablesForThirdPartyTools,
133
134
  interceptTimeout: undefined,
134
135
  interceptUrls: [],
136
+ shouldBypassPayloadValidation:undefined,
137
+ useHostEventsV2:undefined,
138
+ embedExpiryInAuthToken:true
135
139
  };
136
140
  return {
137
141
  type: EmbedEvent.APP_INIT,
@@ -641,6 +645,37 @@ describe('Unit test case for ts embed', () => {
641
645
  });
642
646
  });
643
647
 
648
+ test.each([
649
+ ['not set', undefined, true],
650
+ ['false', false, false],
651
+ ['true', true, true],
652
+ ] as [string, boolean | undefined, boolean][])(
653
+ 'embedExpiryInAuthToken is %s when refreshAuthTokenOnNearExpiry is %s',
654
+ async (_label, refreshAuthTokenOnNearExpiry, expectedEmbedExpiry) => {
655
+ const mockEmbedEventPayload = {
656
+ type: EmbedEvent.APP_INIT,
657
+ data: {},
658
+ };
659
+ const searchEmbed = new AppEmbed(getRootEl(), {
660
+ ...defaultViewConfig,
661
+ refreshAuthTokenOnNearExpiry,
662
+ });
663
+ searchEmbed.render();
664
+ const mockPort: any = {
665
+ postMessage: jest.fn(),
666
+ };
667
+ await executeAfterWait(() => {
668
+ const iframe = getIFrameEl();
669
+ postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort);
670
+ });
671
+ await executeAfterWait(() => {
672
+ expect(mockPort.postMessage).toHaveBeenCalledWith(
673
+ getMockAppInitPayload({ embedExpiryInAuthToken: expectedEmbedExpiry }),
674
+ );
675
+ });
676
+ },
677
+ );
678
+
644
679
  test('when Embed event status have start status', (done) => {
645
680
  const mockEmbedEventPayload = {
646
681
  type: EmbedEvent.Save,
@@ -4504,4 +4539,189 @@ describe('ShowPreRender with UpdateEmbedParams', () => {
4504
4539
  expect(iframe.allow).toContain('clipboard-read');
4505
4540
  expect(iframe.allow).toContain('clipboard-write');
4506
4541
  });
4542
+
4543
+ describe('shouldSkipEvent', () => {
4544
+ beforeAll(() => {
4545
+ init({
4546
+ thoughtSpotHost: 'tshost',
4547
+ authType: AuthType.None,
4548
+ });
4549
+ });
4550
+
4551
+ // Matches the structure produced by createValidationError / embedErrorDetails
4552
+ const makeNestedValidationData = (message = 'invalid payload') => ({
4553
+ type: EmbedEvent.Error,
4554
+ data: {
4555
+ errorType: 'VALIDATION_ERROR',
4556
+ message,
4557
+ code: EmbedErrorCodes.HOST_EVENT_VALIDATION,
4558
+ error: message,
4559
+ },
4560
+ });
4561
+
4562
+ // Matches the flat structure where errorType sits at the top level of data
4563
+ const makeFlatValidationData = (message = 'invalid payload') => ({
4564
+ errorType: EmbedErrorCodes.HOST_EVENT_VALIDATION,
4565
+ message,
4566
+ code: EmbedErrorCodes.HOST_EVENT_VALIDATION,
4567
+ });
4568
+
4569
+ const makeEmbed = (viewConfig: Partial<SearchViewConfig>) => {
4570
+ const embed = new SearchEmbed(getRootEl(), {
4571
+ ...defaultViewConfig,
4572
+ ...viewConfig,
4573
+ });
4574
+ return embed;
4575
+ };
4576
+
4577
+ test('skips Error event and logs warning when useHostEventsV2 is true and shouldBypassPayloadValidation is true', () => {
4578
+ jest.spyOn(logger, 'warn');
4579
+ const errorHandler = jest.fn();
4580
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: true });
4581
+ embed.on(EmbedEvent.Error, errorHandler);
4582
+
4583
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData());
4584
+
4585
+ expect(errorHandler).not.toHaveBeenCalled();
4586
+ expect(logger.warn).toHaveBeenCalledWith(
4587
+ 'Host Event Validation failed: invalid payload',
4588
+ );
4589
+ });
4590
+
4591
+ test('skips Error event when errorType is resolved from data.data.code (nested format)', () => {
4592
+ jest.spyOn(logger, 'warn');
4593
+ const errorHandler = jest.fn();
4594
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: true });
4595
+ embed.on(EmbedEvent.Error, errorHandler);
4596
+
4597
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData('nested error'));
4598
+
4599
+ expect(errorHandler).not.toHaveBeenCalled();
4600
+ expect(logger.warn).toHaveBeenCalledWith(
4601
+ 'Host Event Validation failed: nested error',
4602
+ );
4603
+ });
4604
+
4605
+ test('skips Error event when errorType is resolved from data.errorType (flat format)', () => {
4606
+ jest.spyOn(logger, 'warn');
4607
+ const errorHandler = jest.fn();
4608
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: true });
4609
+ embed.on(EmbedEvent.Error, errorHandler);
4610
+
4611
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeFlatValidationData());
4612
+
4613
+ expect(errorHandler).not.toHaveBeenCalled();
4614
+ });
4615
+
4616
+ test('delivers Error event to handler when useHostEventsV2 is true and shouldBypassPayloadValidation is undefined', () => {
4617
+ const errorHandler = jest.fn();
4618
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: undefined });
4619
+ embed.on(EmbedEvent.Error, errorHandler);
4620
+
4621
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData());
4622
+
4623
+ expect(errorHandler).toHaveBeenCalled();
4624
+ });
4625
+
4626
+ test('delivers Error event to handler when useHostEventsV2 is true and shouldBypassPayloadValidation is false', () => {
4627
+ const errorHandler = jest.fn();
4628
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: false });
4629
+ embed.on(EmbedEvent.Error, errorHandler);
4630
+
4631
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData());
4632
+
4633
+ expect(errorHandler).toHaveBeenCalled();
4634
+ });
4635
+
4636
+ test('skips Error event when useHostEventsV2 is false regardless of shouldBypassPayloadValidation', () => {
4637
+ jest.spyOn(logger, 'warn');
4638
+ const errorHandler = jest.fn();
4639
+ const embed = makeEmbed({ useHostEventsV2: false, shouldBypassPayloadValidation: undefined });
4640
+ embed.on(EmbedEvent.Error, errorHandler);
4641
+
4642
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData());
4643
+
4644
+ expect(errorHandler).not.toHaveBeenCalled();
4645
+ expect(logger.warn).toHaveBeenCalledWith(
4646
+ 'Host Event Validation failed: invalid payload',
4647
+ );
4648
+ });
4649
+
4650
+ test('logs warning with undefined message when flat format has no nested data', () => {
4651
+ jest.spyOn(logger, 'warn');
4652
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: true });
4653
+ embed.on(EmbedEvent.Error, jest.fn());
4654
+
4655
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeFlatValidationData());
4656
+
4657
+ expect(logger.warn).toHaveBeenCalledWith('Host Event Validation failed: undefined');
4658
+ });
4659
+
4660
+ test('skips Error event when useHostEventsV2 is false and shouldBypassPayloadValidation is true', () => {
4661
+ jest.spyOn(logger, 'warn');
4662
+ const errorHandler = jest.fn();
4663
+ const embed = makeEmbed({ useHostEventsV2: false, shouldBypassPayloadValidation: true });
4664
+ embed.on(EmbedEvent.Error, errorHandler);
4665
+
4666
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData());
4667
+
4668
+ expect(errorHandler).not.toHaveBeenCalled();
4669
+ expect(logger.warn).toHaveBeenCalledWith('Host Event Validation failed: invalid payload');
4670
+ });
4671
+
4672
+ test('skips via handleError when shouldBypassPayloadValidation is true', () => {
4673
+ jest.spyOn(logger, 'warn');
4674
+ const errorHandler = jest.fn();
4675
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: true });
4676
+ embed.on(EmbedEvent.Error, errorHandler);
4677
+
4678
+ (embed as any).handleError({
4679
+ type: EmbedEvent.Error,
4680
+ data: {
4681
+ errorType: 'VALIDATION_ERROR',
4682
+ message: 'bad payload',
4683
+ code: EmbedErrorCodes.HOST_EVENT_VALIDATION,
4684
+ error: 'bad payload',
4685
+ },
4686
+ });
4687
+
4688
+ expect(errorHandler).not.toHaveBeenCalled();
4689
+ expect(logger.warn).toHaveBeenCalledWith('Host Event Validation failed: bad payload');
4690
+ });
4691
+
4692
+ test('delivers Error event to EmbedEvent.ALL handler when not skipped', () => {
4693
+ const allHandler = jest.fn();
4694
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: false });
4695
+ embed.on(EmbedEvent.ALL, allHandler);
4696
+
4697
+ (embed as any).executeCallbacks(EmbedEvent.Error, makeNestedValidationData());
4698
+
4699
+ expect(allHandler).toHaveBeenCalled();
4700
+ });
4701
+
4702
+ test('does not skip non-Error events even with HOST_EVENT_VALIDATION error code', () => {
4703
+ const customActionHandler = jest.fn();
4704
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: false });
4705
+ embed.on(EmbedEvent.CustomAction, customActionHandler);
4706
+
4707
+ (embed as any).executeCallbacks(EmbedEvent.CustomAction, {
4708
+ data: { code: EmbedErrorCodes.HOST_EVENT_VALIDATION },
4709
+ });
4710
+
4711
+ expect(customActionHandler).toHaveBeenCalled();
4712
+ });
4713
+
4714
+ test('does not skip Error events with unrelated error codes', () => {
4715
+ const errorHandler = jest.fn();
4716
+ const embed = makeEmbed({ useHostEventsV2: true, shouldBypassPayloadValidation: false });
4717
+ embed.on(EmbedEvent.Error, errorHandler);
4718
+
4719
+ (embed as any).executeCallbacks(EmbedEvent.Error, {
4720
+ errorType: 'SOME_OTHER_ERROR',
4721
+ message: 'something else failed',
4722
+ });
4723
+
4724
+ expect(errorHandler).toHaveBeenCalled();
4725
+ });
4726
+ });
4507
4727
  });
@@ -67,7 +67,7 @@ import {
67
67
  } from '../types';
68
68
  import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
69
69
  import { processEventData, processAuthFailure } from '../utils/processData';
70
- import pkgInfo from '../../package.json';
70
+ import { version } from '../../package.json';
71
71
  import {
72
72
  getAuthPromise, renderInQueue, handleAuth, notifyAuthFailure,
73
73
  getInitPromise,
@@ -80,8 +80,6 @@ import { getPreauthInfo } from '../utils/sessionInfoService';
80
80
  import { HostEventClient } from './hostEventClient/host-event-client';
81
81
  import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept';
82
82
 
83
- const { version } = pkgInfo;
84
-
85
83
  /**
86
84
  * Global prefix for all ThoughtSpot postHash Params.
87
85
  */
@@ -483,7 +481,7 @@ export class TsEmbed {
483
481
  this.embedConfig.customVariablesForThirdPartyTools || {},
484
482
  hiddenListColumns: this.viewConfig.hiddenListColumns || [],
485
483
  customActions: customActionsResult.actions,
486
- embedExpiryInAuthToken: this.viewConfig.refreshAuthTokenOnNearExpiry,
484
+ embedExpiryInAuthToken: this.viewConfig.refreshAuthTokenOnNearExpiry ?? true,
487
485
  ...getInterceptInitData(this.viewConfig),
488
486
  ...getHostEventsConfig(this.viewConfig),
489
487
  };
@@ -1151,6 +1149,18 @@ export class TsEmbed {
1151
1149
  }
1152
1150
  }
1153
1151
 
1152
+ private shouldSkipEvent(eventType: EmbedEvent, data: any): boolean {
1153
+ const errorType = data?.errorType ?? data?.data?.code;
1154
+ if (
1155
+ eventType === EmbedEvent.Error
1156
+ && errorType === EmbedErrorCodes.HOST_EVENT_VALIDATION
1157
+ && (!getHostEventsConfig(this.viewConfig).useHostEventsV2 || getHostEventsConfig(this.viewConfig).shouldBypassPayloadValidation)
1158
+ ) {
1159
+ logger.warn(`Host Event Validation failed: ${data?.data?.message}`);
1160
+ return true;
1161
+ }
1162
+ return false;
1163
+ }
1154
1164
  /**
1155
1165
  * Executes all registered event handlers for a particular event type
1156
1166
  * @param eventType The event type
@@ -1162,6 +1172,7 @@ export class TsEmbed {
1162
1172
  data: any,
1163
1173
  eventPort?: MessagePort | void,
1164
1174
  ): void {
1175
+ if (this.shouldSkipEvent(eventType, data)) return;
1165
1176
  const eventHandlers = this.eventHandlerMap.get(eventType) || [];
1166
1177
  const allHandlers = this.eventHandlerMap.get(EmbedEvent.ALL) || [];
1167
1178
  const callbacks = [...eventHandlers, ...allHandlers];
package/src/index.ts CHANGED
@@ -71,6 +71,14 @@ import {
71
71
  ErrorDetailsTypes,
72
72
  ContextType,
73
73
  AutoMCPFrameRendererViewConfig,
74
+ LegendPosition,
75
+ BackgroundFormatType,
76
+ ConditionalFormattingComparisonType,
77
+ ConditionalFormattingOperator,
78
+ DataLabelFilterOperator,
79
+ TableTheme,
80
+ TableContentDensity,
81
+ VisualizationOverrides,
74
82
  } from './types';
75
83
  import { CustomCssVariables } from './css-variables';
76
84
  import { AnswerService, SessionInterface, UnderlyingDataPoint } from './utils/graphql/answerService/answerService';
@@ -165,6 +173,14 @@ export {
165
173
  EmbedErrorDetailsEvent,
166
174
  ErrorDetailsTypes,
167
175
  AutoMCPFrameRendererViewConfig,
176
+ VisualizationOverrides,
177
+ LegendPosition,
178
+ BackgroundFormatType,
179
+ ConditionalFormattingComparisonType,
180
+ ConditionalFormattingOperator,
181
+ DataLabelFilterOperator,
182
+ TableTheme,
183
+ TableContentDensity,
168
184
  };
169
185
 
170
186
  export { resetCachedAuthToken } from './authToken';
@@ -8,6 +8,7 @@ import {
8
8
  import { AuthType } from './types';
9
9
  import { SessionInfo } from './utils/sessionInfoService';
10
10
  import { logger } from './utils/logger';
11
+ import { version } from '../package.json';
11
12
 
12
13
  const config = {
13
14
  thoughtSpotHost: 'https://10.87.89.232',
@@ -60,6 +61,7 @@ describe('Unit test for mixpanel', () => {
60
61
  clusterName: sessionInfo.clusterName,
61
62
  releaseVersion: sessionInfo.releaseVersion,
62
63
  hostAppUrl: 'localhost',
64
+ sdkVersion: version,
63
65
  });
64
66
  expect(mixpanel.identify).not.toHaveBeenCalledWith(sessionInfo.userGUID);
65
67
  });
@@ -2,6 +2,7 @@ import * as mixpanel from 'mixpanel-browser';
2
2
  import { logger } from './utils/logger';
3
3
  import { SessionInfo } from './utils/sessionInfoService';
4
4
  import { ERROR_MESSAGE } from './errors';
5
+ import { version } from '../package.json';
5
6
 
6
7
  export const EndPoints = {
7
8
  CONFIG: '/callosum/v1/system/config',
@@ -80,6 +81,7 @@ export function initMixpanel(sessionInfo: SessionInfo): void {
80
81
  clusterName: sessionInfo.clusterName,
81
82
  releaseVersion: sessionInfo.releaseVersion,
82
83
  hostAppUrl: window?.location?.host || '',
84
+ sdkVersion: version,
83
85
  });
84
86
  isMixpanelInitialized = true;
85
87
  emptyQueue();
@@ -1,6 +1,6 @@
1
1
  import { has } from 'lodash';
2
2
  import { version } from '../../package.json';
3
- import { Action, AuthType } from '../types';
3
+ import { Action, AuthType, EmbedEvent } from '../types';
4
4
 
5
5
  /**
6
6
  Initialises fetch to the global object
@@ -166,3 +166,35 @@ export const mockSessionInfo = {
166
166
  genNo: 5,
167
167
  },
168
168
  };
169
+
170
+ export const testVisualOverridesInEmbed = async (
171
+ embed: any,
172
+ visualOverrides: any,
173
+ ) => {
174
+ const mockEmbedEventPayload = {
175
+ type: EmbedEvent.APP_INIT,
176
+ data: {},
177
+ };
178
+
179
+ embed.render();
180
+
181
+ const mockPort: any = {
182
+ postMessage: jest.fn(),
183
+ };
184
+
185
+ await executeAfterWait(() => {
186
+ const iframe = getIFrameEl();
187
+ postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort);
188
+ });
189
+
190
+ await executeAfterWait(() => {
191
+ expect(mockPort.postMessage).toHaveBeenCalledWith({
192
+ type: EmbedEvent.APP_INIT,
193
+ data: expect.objectContaining({
194
+ embedParams: expect.objectContaining({
195
+ visualOverridesParams: visualOverrides,
196
+ }),
197
+ }),
198
+ });
199
+ });
200
+ };
@@ -0,0 +1,81 @@
1
+ import 'jest-fetch-mock';
2
+ import * as embedConfigModule from './embed/embedConfig';
3
+ import * as authTokenModule from './authToken';
4
+ import { AuthType } from './types';
5
+ import { tokenizedFetch } from './tokenizedFetch';
6
+
7
+ jest.mock('./embed/embedConfig');
8
+ jest.mock('./authToken');
9
+
10
+ const mockGetEmbedConfig = embedConfigModule.getEmbedConfig as jest.Mock;
11
+ const mockGetAuthenticationToken = authTokenModule.getAuthenticationToken as jest.Mock;
12
+ const mockGetCacheAuthToken = authTokenModule.getCacheAuthToken as jest.Mock;
13
+
14
+ describe('tokenizedFetch', () => {
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ fetchMock.resetMocks();
18
+ });
19
+
20
+ describe('non-cookieless auth', () => {
21
+ beforeEach(() => {
22
+ mockGetEmbedConfig.mockReturnValue({ authType: AuthType.TrustedAuthToken });
23
+ });
24
+
25
+ it('should add Authorization Bearer header when cachedAuthToken exists', async () => {
26
+ mockGetCacheAuthToken.mockReturnValue('my-cached-token');
27
+ fetchMock.mockResponseOnce(JSON.stringify({}));
28
+
29
+ await tokenizedFetch('https://example.com/api', { method: 'GET' });
30
+
31
+ expect(fetchMock).toHaveBeenCalledTimes(1);
32
+ const request = fetchMock.mock.calls[0][0] as Request;
33
+ expect(request).toBeInstanceOf(Request);
34
+ expect(request.headers.get('Authorization')).toBe('Bearer my-cached-token');
35
+ });
36
+
37
+ it('should fetch with credentials include when no cachedAuthToken', async () => {
38
+ mockGetCacheAuthToken.mockReturnValue(null);
39
+ fetchMock.mockResponseOnce(JSON.stringify({}));
40
+
41
+ await tokenizedFetch('https://example.com/api', { method: 'GET' });
42
+
43
+ expect(fetchMock).toHaveBeenCalledWith('https://example.com/api', {
44
+ credentials: 'include',
45
+ method: 'GET',
46
+ });
47
+ });
48
+ });
49
+
50
+ describe('cookieless auth (TrustedAuthTokenCookieless)', () => {
51
+ beforeEach(() => {
52
+ mockGetEmbedConfig.mockReturnValue({
53
+ authType: AuthType.TrustedAuthTokenCookieless,
54
+ thoughtSpotHost: 'https://example.com',
55
+ });
56
+ });
57
+
58
+ it('should add Authorization Bearer header from getAuthenticationToken', async () => {
59
+ mockGetAuthenticationToken.mockResolvedValue('cookieless-token');
60
+ fetchMock.mockResponseOnce(JSON.stringify({}));
61
+
62
+ await tokenizedFetch('https://example.com/api', { method: 'POST' });
63
+
64
+ expect(mockGetAuthenticationToken).toHaveBeenCalled();
65
+ const request = fetchMock.mock.calls[0][0] as Request;
66
+ expect(request).toBeInstanceOf(Request);
67
+ expect(request.headers.get('Authorization')).toBe('Bearer cookieless-token');
68
+ });
69
+
70
+ it('should not add Authorization header when getAuthenticationToken returns null', async () => {
71
+ mockGetAuthenticationToken.mockResolvedValue(null);
72
+ fetchMock.mockResponseOnce(JSON.stringify({}));
73
+
74
+ await tokenizedFetch('https://example.com/api', { method: 'POST' });
75
+
76
+ const request = fetchMock.mock.calls[0][0] as Request;
77
+ expect(request).toBeInstanceOf(Request);
78
+ expect(request.headers.get('Authorization')).toBeNull();
79
+ });
80
+ });
81
+ });
@@ -1,4 +1,4 @@
1
- import { getAuthenticationToken } from './authToken';
1
+ import { getAuthenticationToken, getCacheAuthToken } from './authToken';
2
2
  import { getEmbedConfig } from './embed/embedConfig';
3
3
 
4
4
  import { AuthType } from './types';
@@ -20,18 +20,21 @@ import { AuthType } from './types';
20
20
  */
21
21
  export const tokenizedFetch: typeof fetch = async (input, init): Promise<Response> => {
22
22
  const embedConfig = getEmbedConfig();
23
+ const options: RequestInit = { ...init };
24
+ let token: string | undefined;
23
25
  if (embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) {
24
- return fetch(input, {
25
- // ensure cookies are included for the non cookie-less api calls.
26
- credentials: 'include',
27
- ...init,
28
- });
26
+ token = getCacheAuthToken();
27
+ if (!token) {
28
+ return fetch(input, { ...options, credentials: 'include' });
29
+ }
30
+ } else {
31
+ token = await getAuthenticationToken(embedConfig);
29
32
  }
33
+ const req = new Request(input, options);
30
34
 
31
- const req = new Request(input, init);
32
- const authToken = await getAuthenticationToken(embedConfig);
33
- if (authToken) {
34
- req.headers.append('Authorization', `Bearer ${authToken}`);
35
+ if (token) {
36
+ req.headers.append('Authorization', `Bearer ${token}`);
35
37
  }
38
+
36
39
  return fetch(req);
37
- };
40
+ };