@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.
- package/cjs/package.json +1 -1
- package/cjs/src/auth.d.ts.map +1 -1
- package/cjs/src/auth.js +11 -1
- package/cjs/src/auth.js.map +1 -1
- package/cjs/src/auth.spec.js +38 -0
- package/cjs/src/auth.spec.js.map +1 -1
- package/cjs/src/authToken.d.ts +2 -0
- package/cjs/src/authToken.d.ts.map +1 -1
- package/cjs/src/authToken.js +7 -5
- package/cjs/src/authToken.js.map +1 -1
- package/cjs/src/embed/app.d.ts +7 -2
- package/cjs/src/embed/app.d.ts.map +1 -1
- package/cjs/src/embed/app.js +4 -1
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/app.spec.js +121 -0
- package/cjs/src/embed/app.spec.js.map +1 -1
- package/cjs/src/embed/conversation.d.ts +2 -1
- package/cjs/src/embed/conversation.d.ts.map +1 -1
- package/cjs/src/embed/conversation.js.map +1 -1
- package/cjs/src/embed/liveboard.d.ts +1 -1
- package/cjs/src/embed/liveboard.d.ts.map +1 -1
- package/cjs/src/embed/liveboard.js +4 -1
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +32 -0
- package/cjs/src/embed/liveboard.spec.js.map +1 -1
- package/cjs/src/embed/search.d.ts +24 -1
- package/cjs/src/embed/search.d.ts.map +1 -1
- package/cjs/src/embed/search.js +15 -2
- package/cjs/src/embed/search.js.map +1 -1
- package/cjs/src/embed/search.spec.js +99 -0
- package/cjs/src/embed/search.spec.js.map +1 -1
- package/cjs/src/embed/spotter-utils.d.ts +3 -0
- package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
- package/cjs/src/embed/spotter-utils.js +11 -3
- package/cjs/src/embed/spotter-utils.js.map +1 -1
- package/cjs/src/embed/spotter-utils.spec.js +51 -0
- package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
- package/cjs/src/embed/ts-embed.d.ts +1 -0
- package/cjs/src/embed/ts-embed.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.js +17 -5
- package/cjs/src/embed/ts-embed.js.map +1 -1
- package/cjs/src/embed/ts-embed.spec.js +168 -0
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/index.d.ts +2 -2
- package/cjs/src/index.d.ts.map +1 -1
- package/cjs/src/index.js +8 -1
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/mixpanel-service.d.ts.map +1 -1
- package/cjs/src/mixpanel-service.js +2 -0
- package/cjs/src/mixpanel-service.js.map +1 -1
- package/cjs/src/mixpanel-service.spec.js +2 -0
- package/cjs/src/mixpanel-service.spec.js.map +1 -1
- package/cjs/src/test/test-utils.d.ts +1 -0
- package/cjs/src/test/test-utils.d.ts.map +1 -1
- package/cjs/src/test/test-utils.js +26 -1
- package/cjs/src/test/test-utils.js.map +1 -1
- package/cjs/src/tokenizedFetch.d.ts.map +1 -1
- package/cjs/src/tokenizedFetch.js +12 -9
- package/cjs/src/tokenizedFetch.js.map +1 -1
- package/cjs/src/tokenizedFetch.spec.d.ts +2 -0
- package/cjs/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/cjs/src/tokenizedFetch.spec.js +68 -0
- package/cjs/src/tokenizedFetch.spec.js.map +1 -0
- package/cjs/src/types.d.ts +466 -13
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +149 -5
- package/cjs/src/types.js.map +1 -1
- package/dist/{index-CFZ7RDZ9.js → index-Ck-r09gt.js} +1 -1
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/authToken.d.ts +2 -0
- package/dist/src/authToken.d.ts.map +1 -1
- package/dist/src/embed/app.d.ts +7 -2
- package/dist/src/embed/app.d.ts.map +1 -1
- package/dist/src/embed/conversation.d.ts +2 -1
- package/dist/src/embed/conversation.d.ts.map +1 -1
- package/dist/src/embed/liveboard.d.ts +1 -1
- package/dist/src/embed/liveboard.d.ts.map +1 -1
- package/dist/src/embed/search.d.ts +24 -1
- package/dist/src/embed/search.d.ts.map +1 -1
- package/dist/src/embed/spotter-utils.d.ts +3 -0
- package/dist/src/embed/spotter-utils.d.ts.map +1 -1
- package/dist/src/embed/ts-embed.d.ts +1 -0
- package/dist/src/embed/ts-embed.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/mixpanel-service.d.ts.map +1 -1
- package/dist/src/test/test-utils.d.ts +1 -0
- package/dist/src/test/test-utils.d.ts.map +1 -1
- package/dist/src/tokenizedFetch.d.ts.map +1 -1
- package/dist/src/tokenizedFetch.spec.d.ts +2 -0
- package/dist/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/dist/src/types.d.ts +466 -13
- package/dist/src/types.d.ts.map +1 -1
- package/dist/tsembed-react.es.js +226 -29
- package/dist/tsembed-react.js +225 -28
- package/dist/tsembed.es.js +227 -30
- package/dist/tsembed.js +225 -28
- package/dist/visual-embed-sdk-react-full.d.ts +469 -15
- package/dist/visual-embed-sdk-react.d.ts +469 -15
- package/dist/visual-embed-sdk.d.ts +497 -15
- package/lib/package.json +1 -1
- package/lib/src/auth.d.ts.map +1 -1
- package/lib/src/auth.js +12 -2
- package/lib/src/auth.js.map +1 -1
- package/lib/src/auth.spec.js +38 -0
- package/lib/src/auth.spec.js.map +1 -1
- package/lib/src/authToken.d.ts +2 -0
- package/lib/src/authToken.d.ts.map +1 -1
- package/lib/src/authToken.js +2 -2
- package/lib/src/authToken.js.map +1 -1
- package/lib/src/embed/app.d.ts +7 -2
- package/lib/src/embed/app.d.ts.map +1 -1
- package/lib/src/embed/app.js +4 -1
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/app.spec.js +122 -1
- package/lib/src/embed/app.spec.js.map +1 -1
- package/lib/src/embed/conversation.d.ts +2 -1
- package/lib/src/embed/conversation.d.ts.map +1 -1
- package/lib/src/embed/conversation.js.map +1 -1
- package/lib/src/embed/liveboard.d.ts +1 -1
- package/lib/src/embed/liveboard.d.ts.map +1 -1
- package/lib/src/embed/liveboard.js +4 -1
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +32 -0
- package/lib/src/embed/liveboard.spec.js.map +1 -1
- package/lib/src/embed/search.d.ts +24 -1
- package/lib/src/embed/search.d.ts.map +1 -1
- package/lib/src/embed/search.js +15 -2
- package/lib/src/embed/search.js.map +1 -1
- package/lib/src/embed/search.spec.js +100 -1
- package/lib/src/embed/search.spec.js.map +1 -1
- package/lib/src/embed/spotter-utils.d.ts +3 -0
- package/lib/src/embed/spotter-utils.d.ts.map +1 -1
- package/lib/src/embed/spotter-utils.js +11 -3
- package/lib/src/embed/spotter-utils.js.map +1 -1
- package/lib/src/embed/spotter-utils.spec.js +51 -0
- package/lib/src/embed/spotter-utils.spec.js.map +1 -1
- package/lib/src/embed/ts-embed.d.ts +1 -0
- package/lib/src/embed/ts-embed.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.js +16 -4
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +168 -0
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +2 -2
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +2 -2
- package/lib/src/index.js.map +1 -1
- package/lib/src/mixpanel-service.d.ts.map +1 -1
- package/lib/src/mixpanel-service.js +2 -0
- package/lib/src/mixpanel-service.js.map +1 -1
- package/lib/src/mixpanel-service.spec.js +2 -0
- package/lib/src/mixpanel-service.spec.js.map +1 -1
- package/lib/src/test/test-utils.d.ts +1 -0
- package/lib/src/test/test-utils.d.ts.map +1 -1
- package/lib/src/test/test-utils.js +25 -1
- package/lib/src/test/test-utils.js.map +1 -1
- package/lib/src/tokenizedFetch.d.ts.map +1 -1
- package/lib/src/tokenizedFetch.js +13 -10
- package/lib/src/tokenizedFetch.js.map +1 -1
- package/lib/src/tokenizedFetch.spec.d.ts +2 -0
- package/lib/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/lib/src/tokenizedFetch.spec.js +65 -0
- package/lib/src/tokenizedFetch.spec.js.map +1 -0
- package/lib/src/types.d.ts +466 -13
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +148 -4
- package/lib/src/types.js.map +1 -1
- package/lib/src/visual-embed-sdk.d.ts +497 -15
- package/package.json +1 -1
- package/src/auth.spec.ts +55 -1
- package/src/auth.ts +11 -2
- package/src/authToken.ts +2 -2
- package/src/embed/app.spec.ts +154 -0
- package/src/embed/app.ts +13 -2
- package/src/embed/conversation.ts +2 -1
- package/src/embed/liveboard.spec.ts +46 -0
- package/src/embed/liveboard.ts +9 -4
- package/src/embed/search.spec.ts +118 -0
- package/src/embed/search.ts +43 -1
- package/src/embed/spotter-utils.spec.ts +52 -0
- package/src/embed/spotter-utils.ts +19 -3
- package/src/embed/ts-embed.spec.ts +220 -0
- package/src/embed/ts-embed.ts +15 -4
- package/src/index.ts +16 -0
- package/src/mixpanel-service.spec.ts +2 -0
- package/src/mixpanel-service.ts +2 -0
- package/src/test/test-utils.ts +33 -1
- package/src/tokenizedFetch.spec.ts +81 -0
- package/src/tokenizedFetch.ts +14 -11
- 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 & {
|
|
28
|
-
|
|
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)
|
|
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
|
});
|
package/src/embed/ts-embed.ts
CHANGED
|
@@ -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
|
|
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
|
});
|
package/src/mixpanel-service.ts
CHANGED
|
@@ -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();
|
package/src/test/test-utils.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/tokenizedFetch.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
credentials: 'include'
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
+
};
|