@thoughtspot/visual-embed-sdk 1.53.0 → 1.53.1

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 (151) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/contracts/contracts.spec.js +4 -0
  3. package/cjs/src/contracts/contracts.spec.js.map +1 -1
  4. package/cjs/src/contracts/host-event-contracts.d.ts +10 -0
  5. package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -1
  6. package/cjs/src/embed/app.d.ts +1 -0
  7. package/cjs/src/embed/app.d.ts.map +1 -1
  8. package/cjs/src/embed/app.js.map +1 -1
  9. package/cjs/src/embed/conversation.d.ts +14 -0
  10. package/cjs/src/embed/conversation.d.ts.map +1 -1
  11. package/cjs/src/embed/conversation.js.map +1 -1
  12. package/cjs/src/embed/conversation.spec.js +22 -0
  13. package/cjs/src/embed/conversation.spec.js.map +1 -1
  14. package/cjs/src/embed/host-events.spec.js +32 -0
  15. package/cjs/src/embed/host-events.spec.js.map +1 -1
  16. package/cjs/src/embed/liveboard.d.ts +2 -16
  17. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  18. package/cjs/src/embed/liveboard.js +29 -10
  19. package/cjs/src/embed/liveboard.js.map +1 -1
  20. package/cjs/src/embed/liveboard.spec.js +97 -0
  21. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  22. package/cjs/src/embed/spotter-utils.d.ts +1 -0
  23. package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
  24. package/cjs/src/embed/spotter-utils.js +7 -1
  25. package/cjs/src/embed/spotter-utils.js.map +1 -1
  26. package/cjs/src/embed/spotter-utils.spec.js +19 -0
  27. package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
  28. package/cjs/src/embed/ts-embed.d.ts +21 -16
  29. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  30. package/cjs/src/embed/ts-embed.js +77 -63
  31. package/cjs/src/embed/ts-embed.js.map +1 -1
  32. package/cjs/src/embed/ts-embed.spec.js +240 -7
  33. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  34. package/cjs/src/index.d.ts +2 -2
  35. package/cjs/src/index.d.ts.map +1 -1
  36. package/cjs/src/index.js.map +1 -1
  37. package/cjs/src/react/all-types-export.d.ts +1 -1
  38. package/cjs/src/react/all-types-export.d.ts.map +1 -1
  39. package/cjs/src/react/all-types-export.js.map +1 -1
  40. package/cjs/src/types.d.ts +69 -2
  41. package/cjs/src/types.d.ts.map +1 -1
  42. package/cjs/src/types.js +67 -0
  43. package/cjs/src/types.js.map +1 -1
  44. package/cjs/src/utils/runtimeTelemetry.d.ts +11 -0
  45. package/cjs/src/utils/runtimeTelemetry.d.ts.map +1 -0
  46. package/cjs/src/utils/runtimeTelemetry.js +34 -0
  47. package/cjs/src/utils/runtimeTelemetry.js.map +1 -0
  48. package/cjs/src/utils/runtimeTelemetry.spec.d.ts +2 -0
  49. package/cjs/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
  50. package/cjs/src/utils/runtimeTelemetry.spec.js +124 -0
  51. package/cjs/src/utils/runtimeTelemetry.spec.js.map +1 -0
  52. package/dist/{index-CyDR8j_0.js → index-CrBnZBZv.js} +1 -1
  53. package/dist/src/contracts/host-event-contracts.d.ts +10 -0
  54. package/dist/src/contracts/host-event-contracts.d.ts.map +1 -1
  55. package/dist/src/embed/app.d.ts +1 -0
  56. package/dist/src/embed/app.d.ts.map +1 -1
  57. package/dist/src/embed/conversation.d.ts +14 -0
  58. package/dist/src/embed/conversation.d.ts.map +1 -1
  59. package/dist/src/embed/liveboard.d.ts +2 -16
  60. package/dist/src/embed/liveboard.d.ts.map +1 -1
  61. package/dist/src/embed/spotter-utils.d.ts +1 -0
  62. package/dist/src/embed/spotter-utils.d.ts.map +1 -1
  63. package/dist/src/embed/ts-embed.d.ts +21 -16
  64. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  65. package/dist/src/index.d.ts +2 -2
  66. package/dist/src/index.d.ts.map +1 -1
  67. package/dist/src/react/all-types-export.d.ts +1 -1
  68. package/dist/src/react/all-types-export.d.ts.map +1 -1
  69. package/dist/src/types.d.ts +69 -2
  70. package/dist/src/types.d.ts.map +1 -1
  71. package/dist/src/utils/runtimeTelemetry.d.ts +11 -0
  72. package/dist/src/utils/runtimeTelemetry.d.ts.map +1 -0
  73. package/dist/src/utils/runtimeTelemetry.spec.d.ts +2 -0
  74. package/dist/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
  75. package/dist/tsembed-react.es.js +247 -78
  76. package/dist/tsembed-react.js +246 -77
  77. package/dist/tsembed.es.js +247 -78
  78. package/dist/tsembed.js +246 -77
  79. package/dist/visual-embed-sdk-react-full.d.ts +116 -18
  80. package/dist/visual-embed-sdk-react.d.ts +116 -18
  81. package/dist/visual-embed-sdk.d.ts +116 -33
  82. package/lib/package.json +1 -1
  83. package/lib/src/contracts/contracts.spec.js +4 -0
  84. package/lib/src/contracts/contracts.spec.js.map +1 -1
  85. package/lib/src/contracts/host-event-contracts.d.ts +10 -0
  86. package/lib/src/contracts/host-event-contracts.d.ts.map +1 -1
  87. package/lib/src/embed/app.d.ts +1 -0
  88. package/lib/src/embed/app.d.ts.map +1 -1
  89. package/lib/src/embed/app.js.map +1 -1
  90. package/lib/src/embed/conversation.d.ts +14 -0
  91. package/lib/src/embed/conversation.d.ts.map +1 -1
  92. package/lib/src/embed/conversation.js.map +1 -1
  93. package/lib/src/embed/conversation.spec.js +22 -0
  94. package/lib/src/embed/conversation.spec.js.map +1 -1
  95. package/lib/src/embed/host-events.spec.js +32 -0
  96. package/lib/src/embed/host-events.spec.js.map +1 -1
  97. package/lib/src/embed/liveboard.d.ts +2 -16
  98. package/lib/src/embed/liveboard.d.ts.map +1 -1
  99. package/lib/src/embed/liveboard.js +28 -10
  100. package/lib/src/embed/liveboard.js.map +1 -1
  101. package/lib/src/embed/liveboard.spec.js +97 -0
  102. package/lib/src/embed/liveboard.spec.js.map +1 -1
  103. package/lib/src/embed/spotter-utils.d.ts +1 -0
  104. package/lib/src/embed/spotter-utils.d.ts.map +1 -1
  105. package/lib/src/embed/spotter-utils.js +7 -1
  106. package/lib/src/embed/spotter-utils.js.map +1 -1
  107. package/lib/src/embed/spotter-utils.spec.js +19 -0
  108. package/lib/src/embed/spotter-utils.spec.js.map +1 -1
  109. package/lib/src/embed/ts-embed.d.ts +21 -16
  110. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  111. package/lib/src/embed/ts-embed.js +78 -64
  112. package/lib/src/embed/ts-embed.js.map +1 -1
  113. package/lib/src/embed/ts-embed.spec.js +240 -7
  114. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  115. package/lib/src/index.d.ts +2 -2
  116. package/lib/src/index.d.ts.map +1 -1
  117. package/lib/src/index.js.map +1 -1
  118. package/lib/src/react/all-types-export.d.ts +1 -1
  119. package/lib/src/react/all-types-export.d.ts.map +1 -1
  120. package/lib/src/react/all-types-export.js.map +1 -1
  121. package/lib/src/types.d.ts +69 -2
  122. package/lib/src/types.d.ts.map +1 -1
  123. package/lib/src/types.js +67 -0
  124. package/lib/src/types.js.map +1 -1
  125. package/lib/src/utils/runtimeTelemetry.d.ts +11 -0
  126. package/lib/src/utils/runtimeTelemetry.d.ts.map +1 -0
  127. package/lib/src/utils/runtimeTelemetry.js +28 -0
  128. package/lib/src/utils/runtimeTelemetry.js.map +1 -0
  129. package/lib/src/utils/runtimeTelemetry.spec.d.ts +2 -0
  130. package/lib/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
  131. package/lib/src/utils/runtimeTelemetry.spec.js +122 -0
  132. package/lib/src/utils/runtimeTelemetry.spec.js.map +1 -0
  133. package/lib/src/visual-embed-sdk.d.ts +116 -33
  134. package/package.json +1 -1
  135. package/src/contracts/contracts.spec.ts +4 -0
  136. package/src/contracts/host-event-contracts.ts +13 -0
  137. package/src/embed/app.ts +1 -0
  138. package/src/embed/conversation.spec.ts +24 -0
  139. package/src/embed/conversation.ts +14 -0
  140. package/src/embed/host-events.spec.ts +44 -0
  141. package/src/embed/liveboard.spec.ts +111 -0
  142. package/src/embed/liveboard.ts +34 -10
  143. package/src/embed/spotter-utils.spec.ts +20 -0
  144. package/src/embed/spotter-utils.ts +12 -1
  145. package/src/embed/ts-embed.spec.ts +297 -8
  146. package/src/embed/ts-embed.ts +79 -64
  147. package/src/index.ts +2 -0
  148. package/src/react/all-types-export.ts +1 -0
  149. package/src/types.ts +68 -0
  150. package/src/utils/runtimeTelemetry.spec.ts +148 -0
  151. package/src/utils/runtimeTelemetry.ts +50 -0
@@ -2728,12 +2728,11 @@ describe('Unit test case for ts embed', () => {
2728
2728
  contentRect: { height: 297, width: 987 },
2729
2729
  },
2730
2730
  ]);
2731
-
2732
2731
  expect(preRenderWrapper.style.height).toEqual(`${297}px`);
2733
2732
  expect(preRenderWrapper.style.width).toEqual(`${987}px`);
2734
2733
 
2735
2734
  expect(preRenderWrapper.style.opacity).toBe('');
2736
- expect(preRenderWrapper.style.pointerEvents).toBe('');
2735
+ expect(preRenderWrapper.style.pointerEvents).toBe('auto');
2737
2736
  expect(preRenderWrapper.style.zIndex).toBe('');
2738
2737
 
2739
2738
  libEmbed.hidePreRender();
@@ -2770,7 +2769,7 @@ describe('Unit test case for ts embed', () => {
2770
2769
  const preRenderWrapper = document.getElementById(preRenderIds.wrapper);
2771
2770
 
2772
2771
  expect(preRenderWrapper.style.opacity).toBe('');
2773
- expect(preRenderWrapper.style.pointerEvents).toBe('');
2772
+ expect(preRenderWrapper.style.pointerEvents).toBe('auto');
2774
2773
  expect(preRenderWrapper.style.zIndex).toBe('');
2775
2774
  });
2776
2775
 
@@ -2943,6 +2942,216 @@ describe('Unit test case for ts embed', () => {
2943
2942
  (logger.debug as any).mockRestore();
2944
2943
  });
2945
2944
 
2945
+ describe('position tracking (SCAL-338563)', () => {
2946
+ /**
2947
+ * A host app whose page scrolls in an inner element rather than the
2948
+ * document — the shape both reported cases have. The placeholder's
2949
+ * rect is driven off that element's scrollTop, the way a real
2950
+ * layout would move it.
2951
+ */
2952
+ const mountInNestedScroller = () => {
2953
+ createRootEleForEmbed();
2954
+ const scroller = document.createElement('div');
2955
+ scroller.id = 'inner-scroller';
2956
+ document.body.appendChild(scroller);
2957
+ scroller.appendChild(document.getElementById('tsEmbedDiv'));
2958
+
2959
+ let scrollTop = 0;
2960
+ Object.defineProperty(scroller, 'scrollTop', {
2961
+ configurable: true,
2962
+ get: () => scrollTop,
2963
+ set: (value) => {
2964
+ scrollTop = value;
2965
+ },
2966
+ });
2967
+ scroller.getBoundingClientRect = () =>
2968
+ ({
2969
+ x: 0, y: 0, width: 800, height: 600,
2970
+ top: 0, left: 0, bottom: 600, right: 800,
2971
+ } as DOMRect);
2972
+
2973
+ const placeholderTopAtRest = 400;
2974
+ const readPlaceholderTop = () => placeholderTopAtRest - scroller.scrollTop;
2975
+
2976
+ return { scroller, readPlaceholderTop };
2977
+ };
2978
+
2979
+ const stubPlaceholderRect = (embed: any, readTop: () => number) => {
2980
+ const placeholder = embed.getPreRenderPlaceHolderElement() as HTMLElement;
2981
+ placeholder.getBoundingClientRect = () => {
2982
+ const top = readTop();
2983
+ return {
2984
+ x: 0, y: top, width: 800, height: 300,
2985
+ top, left: 0, bottom: top + 300, right: 800,
2986
+ } as DOMRect;
2987
+ };
2988
+ return placeholder;
2989
+ };
2990
+
2991
+ it('should stop observing the placeholder once the frame is hidden', async () => {
2992
+ const { scroller } = mountInNestedScroller();
2993
+
2994
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2995
+ preRenderId: 'observer-stops-on-hide',
2996
+ liveboardId: 'myLiveboardId',
2997
+ });
2998
+ libEmbed.preRender();
2999
+ await waitFor(() => !!getIFrameEl());
3000
+ await libEmbed.showPreRender();
3001
+
3002
+ // Size is the only thing the browser will not handle for us, so
3003
+ // the observer is the one live resource a hide has to release.
3004
+ const observer = (libEmbed as any).resizeObserver;
3005
+ expect(observer).toBeDefined();
3006
+ const disconnectSpy = jest.spyOn(observer, 'disconnect');
3007
+
3008
+ libEmbed.hidePreRender();
3009
+
3010
+ expect(disconnectSpy).toHaveBeenCalled();
3011
+
3012
+ disconnectSpy.mockRestore();
3013
+ libEmbed.destroy();
3014
+ scroller.remove();
3015
+ });
3016
+
3017
+ it('should default the container to the host scrolling ancestor', async () => {
3018
+ const { scroller } = mountInNestedScroller();
3019
+ scroller.style.overflowY = 'auto';
3020
+
3021
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
3022
+ preRenderId: 'auto-scroll-container',
3023
+ liveboardId: 'myLiveboardId',
3024
+ });
3025
+ libEmbed.preRender();
3026
+ await waitFor(() => !!getIFrameEl());
3027
+ await libEmbed.showPreRender();
3028
+
3029
+ const wrapper = document.getElementById(libEmbed.getPreRenderIds().wrapper);
3030
+ // In the scroller, so the browser moves it — no repositioning.
3031
+ expect(wrapper.parentElement).toBe(scroller);
3032
+ expect((libEmbed as any).preRenderContainerEl).toBe(scroller);
3033
+
3034
+ libEmbed.destroy();
3035
+ scroller.remove();
3036
+ });
3037
+
3038
+ it('should still use document.body when nothing scrolls', async () => {
3039
+ createRootEleForEmbed();
3040
+
3041
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
3042
+ preRenderId: 'no-scroller-body',
3043
+ liveboardId: 'myLiveboardId',
3044
+ });
3045
+ libEmbed.preRender();
3046
+ await waitFor(() => !!getIFrameEl());
3047
+ await libEmbed.showPreRender();
3048
+
3049
+ const wrapper = document.getElementById(libEmbed.getPreRenderIds().wrapper);
3050
+ expect(wrapper.parentElement).toBe(document.body);
3051
+
3052
+ libEmbed.destroy();
3053
+ });
3054
+
3055
+ it('should set pointer-events on show rather than dropping the property', async () => {
3056
+ createRootEleForEmbed();
3057
+ const container = document.createElement('div');
3058
+ container.id = 'parking-root';
3059
+ // Usual styling for a parking root: no swallowed clicks.
3060
+ container.style.pointerEvents = 'none';
3061
+ document.body.appendChild(container);
3062
+
3063
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
3064
+ preRenderId: 'pointer-events-restored',
3065
+ liveboardId: 'myLiveboardId',
3066
+ preRenderContainer: '#parking-root',
3067
+ });
3068
+ libEmbed.preRender();
3069
+ await waitFor(() => !!getIFrameEl());
3070
+ await libEmbed.showPreRender();
3071
+
3072
+ const wrapper = document.getElementById(libEmbed.getPreRenderIds().wrapper);
3073
+ // Merely removing the property would leave the container's
3074
+ // inherited `none` in force, and the frame dead to input.
3075
+ expect(wrapper.style.pointerEvents).toBe('auto');
3076
+
3077
+ libEmbed.destroy();
3078
+ container.remove();
3079
+ });
3080
+
3081
+ it('should park a hidden wrapper out of the scrollable area', async () => {
3082
+ createRootEleForEmbed();
3083
+
3084
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
3085
+ preRenderId: 'hidden-parked',
3086
+ liveboardId: 'myLiveboardId',
3087
+ });
3088
+ libEmbed.preRender();
3089
+ await waitFor(() => !!getIFrameEl());
3090
+ await libEmbed.showPreRender();
3091
+
3092
+ const wrapper = document.getElementById(libEmbed.getPreRenderIds().wrapper);
3093
+ expect(wrapper.style.transform).toBe('');
3094
+
3095
+ libEmbed.hidePreRender();
3096
+ // Detached from every scroll container (fixed, resolves to the
3097
+ // viewport), parked for the captured-fixed case, size
3098
+ // preserved.
3099
+ expect(wrapper.style.position).toBe('fixed');
3100
+ expect(wrapper.style.transform).toBe('translateY(-100%)');
3101
+ expect(wrapper.style.height).not.toBe('');
3102
+
3103
+ await libEmbed.showPreRender();
3104
+ expect(wrapper.style.transform).toBe('');
3105
+
3106
+ libEmbed.destroy();
3107
+ });
3108
+
3109
+ it('should not seed the placeholder with the unmeasured wrapper height', async () => {
3110
+ createRootEleForEmbed();
3111
+
3112
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
3113
+ preRenderId: 'unmeasured-height',
3114
+ liveboardId: 'myLiveboardId',
3115
+ fullHeight: true,
3116
+ frameParams: { height: 420 },
3117
+ });
3118
+ libEmbed.preRender();
3119
+ await waitFor(() => !!getIFrameEl());
3120
+
3121
+ const wrapper = document.getElementById(libEmbed.getPreRenderIds().wrapper);
3122
+ // createPreRenderWrapper() seeds 100vh, not a measurement.
3123
+ expect(wrapper.style.height).toBe('100vh');
3124
+
3125
+ await libEmbed.showPreRender();
3126
+
3127
+ const placeholder = (libEmbed as any).getPreRenderPlaceHolderElement() as HTMLElement;
3128
+ expect(placeholder.style.height).toBe('420px');
3129
+
3130
+ libEmbed.destroy();
3131
+ });
3132
+
3133
+ it('should seed the placeholder with a height fullHeight has measured', async () => {
3134
+ createRootEleForEmbed();
3135
+
3136
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
3137
+ preRenderId: 'measured-height',
3138
+ liveboardId: 'myLiveboardId',
3139
+ fullHeight: true,
3140
+ frameParams: { height: 420 },
3141
+ });
3142
+ libEmbed.preRender();
3143
+ await waitFor(() => !!getIFrameEl());
3144
+
3145
+ (libEmbed as any).setIFrameHeight(1180);
3146
+ await libEmbed.showPreRender();
3147
+
3148
+ const placeholder = (libEmbed as any).getPreRenderPlaceHolderElement() as HTMLElement;
3149
+ expect(placeholder.style.height).toBe('1180px');
3150
+
3151
+ libEmbed.destroy();
3152
+ });
3153
+ });
3154
+
2946
3155
  describe('preRenderContainer', () => {
2947
3156
  it('should append preRenderWrapper to document.body when preRenderContainer is not set', async () => {
2948
3157
  createRootEleForEmbed();
@@ -3216,21 +3425,16 @@ describe('Unit test case for ts embed', () => {
3216
3425
 
3217
3426
  // Simulate React remounting the container: the old node (with
3218
3427
  // our wrapper) is detached and a fresh node takes its place.
3219
- const removeSpy = jest.spyOn(oldContainer, 'removeEventListener');
3220
3428
  oldContainer.remove();
3221
3429
  const newContainer = document.createElement('div');
3222
3430
  newContainer.id = 'remount-container';
3223
3431
  document.body.appendChild(newContainer);
3224
- const addSpy = jest.spyOn(newContainer, 'addEventListener');
3225
3432
 
3226
3433
  // A reposition (scroll/resize/re-show) heals the stale ref.
3227
3434
  libEmbed.syncPreRenderStyle();
3228
3435
 
3229
3436
  expect(newContainer.contains(wrapper)).toBe(true);
3230
3437
  expect(oldContainer.contains(wrapper)).toBe(false);
3231
- // The scroll listener is migrated to the live container.
3232
- expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
3233
- expect(addSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
3234
3438
 
3235
3439
  libEmbed.destroy();
3236
3440
  newContainer.remove();
@@ -7372,3 +7576,88 @@ describe('excludeConfigFromURL', () => {
7372
7576
  });
7373
7577
  });
7374
7578
  });
7579
+
7580
+ describe('visual-sdk-embed-create does not upload runtime filter or parameter data', () => {
7581
+ // Spied per test: an earlier suite calls jest.restoreAllMocks(), which
7582
+ // would unwire a spy created at collection time.
7583
+ let mockMixPanelEvent: jest.SpyInstance;
7584
+
7585
+ const runtimeFilters: RuntimeFilter[] = [
7586
+ {
7587
+ columnName: 'Patient SSN',
7588
+ operator: RuntimeFilterOp.EQ,
7589
+ values: ['123-45-6789'],
7590
+ },
7591
+ ];
7592
+ const runtimeParameters: RuntimeParameter[] = [
7593
+ {
7594
+ name: 'Sales Region',
7595
+ value: 'EMEA',
7596
+ },
7597
+ ];
7598
+
7599
+ beforeAll(() => {
7600
+ init({
7601
+ thoughtSpotHost: 'tshost',
7602
+ authType: AuthType.None,
7603
+ });
7604
+ });
7605
+
7606
+ beforeEach(() => {
7607
+ document.body.innerHTML = getDocumentBody();
7608
+ mockMixPanelEvent = jest.spyOn(mixpanelInstance, 'uploadMixpanelEvent');
7609
+ });
7610
+
7611
+ afterEach(() => {
7612
+ mockMixPanelEvent.mockRestore();
7613
+ });
7614
+
7615
+ const getEmbedCreateProps = () => {
7616
+ const call = mockMixPanelEvent.mock.calls.find(
7617
+ ([eventId]) => eventId === MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE,
7618
+ );
7619
+ expect(call).toBeDefined();
7620
+ return call[1];
7621
+ };
7622
+
7623
+ test('uploads the summary instead of the filters and parameters', () => {
7624
+ new LiveboardEmbed(getRootEl(), {
7625
+ liveboardId: 'lb-guid',
7626
+ runtimeFilters,
7627
+ runtimeParameters,
7628
+ } as LiveboardViewConfig);
7629
+
7630
+ expect(getEmbedCreateProps()).toEqual(
7631
+ expect.objectContaining({
7632
+ liveboardId: 'lb-guid',
7633
+ runtimeFilters: { count: 1, operators: ['EQ'] },
7634
+ runtimeParameters: { count: 1, applicabilityLevels: [] },
7635
+ }),
7636
+ );
7637
+ });
7638
+
7639
+ test('no column name, parameter name or operand reaches mixpanel', () => {
7640
+ new LiveboardEmbed(getRootEl(), {
7641
+ liveboardId: 'lb-guid',
7642
+ runtimeFilters,
7643
+ runtimeParameters,
7644
+ } as LiveboardViewConfig);
7645
+
7646
+ const uploaded = JSON.stringify(getEmbedCreateProps());
7647
+ ['Patient SSN', '123-45-6789', 'Sales Region', 'EMEA'].forEach((customerValue) => {
7648
+ expect(uploaded).not.toContain(customerValue);
7649
+ });
7650
+ });
7651
+
7652
+ test('an embed with no runtime filters is unchanged', () => {
7653
+ new LiveboardEmbed(getRootEl(), {
7654
+ liveboardId: 'lb-guid',
7655
+ } as LiveboardViewConfig);
7656
+
7657
+ const props = getEmbedCreateProps();
7658
+ expect(props).toEqual({
7659
+ liveboardId: 'lb-guid',
7660
+ embedComponentType: 'LiveboardEmbed',
7661
+ });
7662
+ });
7663
+ });
@@ -45,6 +45,7 @@ import {
45
45
  getHostEventsConfig,
46
46
  getValueFromWindow,
47
47
  deserializeParam,
48
+ getScrollableAncestors,
48
49
  setParamIfDefined,
49
50
  } from '../utils';
50
51
  import { getCustomActions } from '../utils/custom-actions';
@@ -80,6 +81,7 @@ import {
80
81
  BaseViewConfig,
81
82
  } from '../types';
82
83
  import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
84
+ import { summarizeRuntimeDataForTelemetry } from '../utils/runtimeTelemetry';
83
85
  import { processEventData, processAuthFailure } from '../utils/processData';
84
86
  import { version } from '../utils/sdk-version';
85
87
  import {
@@ -116,6 +118,9 @@ const TS_EMBED_ID = '_thoughtspot-embed';
116
118
  */
117
119
  const PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY = 'tsEmbedOriginalPosition';
118
120
  const PRERENDER_WRAPPER_ID_PREFIX = 'tsEmbed-pre-render-wrapper-';
121
+ const PRERENDER_PARKED_TRANSFORM = 'translateY(-100%)';
122
+ /** The height createPreRenderWrapper() seeds before anything is measured. */
123
+ const UNMEASURED_WRAPPER_HEIGHT = '100vh';
119
124
 
120
125
  // The container applies UpdateEmbedParams through React state, so a delivered
121
126
  // post is not the same as the params being in effect.
@@ -325,8 +330,7 @@ export class TsEmbed {
325
330
 
326
331
  private preRenderContainerEl: HTMLElement = document.body;
327
332
 
328
- private containerScrollListener: (() => void) | null = null;
329
-
333
+ /** Last height the embedded app reported, once fullHeight has measured one. */
330
334
  protected hostEventClient: HostEventClient;
331
335
 
332
336
  protected isReadyForRenderPromise;
@@ -347,9 +351,10 @@ export class TsEmbed {
347
351
  ...viewConfig,
348
352
  };
349
353
  this.registerAppInit();
350
- uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE, {
351
- ...viewConfig,
352
- });
354
+ uploadMixpanelEvent(
355
+ MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE,
356
+ summarizeRuntimeDataForTelemetry(viewConfig),
357
+ );
353
358
  const embedConfig = getEmbedConfig();
354
359
  if (embedConfig) {
355
360
  this.embedConfig = embedConfig;
@@ -1283,7 +1288,7 @@ export class TsEmbed {
1283
1288
  top: '0',
1284
1289
  left: '0',
1285
1290
  width: '100vw',
1286
- height: '100vh',
1291
+ height: UNMEASURED_WRAPPER_HEIGHT,
1287
1292
  };
1288
1293
  setStyleProperties(preRenderWrapper, initialPreRenderWrapperStyle);
1289
1294
 
@@ -1369,10 +1374,12 @@ export class TsEmbed {
1369
1374
  }
1370
1375
 
1371
1376
  /**
1372
- * Resolves the configured preRenderContainer to a live element, falling
1373
- * back to `document.body`. A string selector is re-queried on every call so
1374
- * a remounted container (E.g.: React replacing the node) resolves to the
1375
- * fresh element; an element passed directly cannot be re-resolved.
1377
+ * Resolves the configured preRenderContainer, or defaults to the host's
1378
+ * nearest scrolling ancestor (document.body if none). The absolutely
1379
+ * positioned wrapper only follows the page while it sits inside what
1380
+ * scrolls, so body is right only when the document itself scrolls — an inner
1381
+ * scroller left the frame pinned to the viewport (SCAL-338563). A string
1382
+ * selector is re-queried each call so a remounted container resolves fresh.
1376
1383
  */
1377
1384
  private resolvePreRenderContainerTarget(): HTMLElement {
1378
1385
  const containerConfig = this.getPreRenderConfig().containerSelector;
@@ -1389,7 +1396,13 @@ export class TsEmbed {
1389
1396
  } else if (containerConfig) {
1390
1397
  container = containerConfig;
1391
1398
  }
1392
- return (container as HTMLElement) ?? document.body;
1399
+ if (container) {
1400
+ return container as HTMLElement;
1401
+ }
1402
+ if (!this.hostElement) {
1403
+ return document.body;
1404
+ }
1405
+ return getScrollableAncestors(this.hostElement)[0] ?? document.body;
1393
1406
  }
1394
1407
 
1395
1408
  private inheritPreRenderContainer(): void {
@@ -1443,8 +1456,9 @@ export class TsEmbed {
1443
1456
  * Re-attaches the wrapper to a live container when the previously resolved
1444
1457
  * one has been detached or no longer holds the wrapper — e.g. the host app
1445
1458
  * remounted a custom preRenderContainer, which would otherwise leave a stale
1446
- * reference and collapse the wrapper. Only string selectors can be
1447
- * re-resolved; a container passed as an element is left untouched.
1459
+ * reference and collapse the wrapper. Selectors and the auto-resolved
1460
+ * scrolling ancestor are both re-resolved; a container passed as an element
1461
+ * is left untouched, as there is nothing to re-query.
1448
1462
  */
1449
1463
  private reconcilePreRenderContainer(): void {
1450
1464
  const wrapper = this.preRenderWrapper;
@@ -1463,14 +1477,6 @@ export class TsEmbed {
1463
1477
  if (resolved === stored && stored.contains(wrapper)) {
1464
1478
  return;
1465
1479
  }
1466
- if (this.containerScrollListener && stored !== resolved) {
1467
- if (stored !== document.body) {
1468
- stored.removeEventListener('scroll', this.containerScrollListener);
1469
- }
1470
- if (resolved !== document.body) {
1471
- resolved.addEventListener('scroll', this.containerScrollListener);
1472
- }
1473
- }
1474
1480
  this.preRenderContainerEl = resolved;
1475
1481
  this.applyPreRenderContainerPositioning();
1476
1482
  if (wrapper.parentNode !== resolved) {
@@ -1539,10 +1545,11 @@ export class TsEmbed {
1539
1545
  */
1540
1546
  protected setIFrameHeight(height: number | string): void {
1541
1547
  if (this.isPreRendered) {
1548
+ const next = getCssDimension(height);
1542
1549
  if (this.insertedDomEl) {
1543
- (this.insertedDomEl as HTMLElement).style.height = getCssDimension(height);
1550
+ (this.insertedDomEl as HTMLElement).style.height = next;
1544
1551
  } else if (this.preRenderWrapper) {
1545
- this.preRenderWrapper.style.height = getCssDimension(height);
1552
+ this.preRenderWrapper.style.height = next;
1546
1553
  }
1547
1554
  } else {
1548
1555
  // normal (non-preRender) mode: size the iframe directly
@@ -2113,15 +2120,27 @@ export class TsEmbed {
2113
2120
  }
2114
2121
 
2115
2122
  /**
2116
- * Detaches and clears the container scroll listener, if one is attached.
2117
- */
2118
- private removeContainerScrollListener(): void {
2119
- if (!this.containerScrollListener) {
2123
+ * Re-sizes the wrapper when its placeholder changes size.
2124
+ *
2125
+ * Position deliberately has no equivalent. An absolutely positioned wrapper
2126
+ * follows its containing block for free, so a correctly placed frame is the
2127
+ * browser's job and costs nothing; repositioning it on scroll would mean a
2128
+ * layout read and a style write per frame to compensate for placing it in
2129
+ * the wrong box. Size is the one thing the browser will not do for us: the
2130
+ * placeholder grows when the embedded app reports a new height, and the
2131
+ * wrapper has to be told.
2132
+ */
2133
+ private observePreRenderSize(): void {
2134
+ if (this.getPreRenderConfig().doNotTrackSize || typeof ResizeObserver === 'undefined') {
2135
+ return;
2136
+ }
2137
+ const placeholder = this.getPreRenderPlaceHolderElement();
2138
+ if (!placeholder) {
2120
2139
  return;
2121
2140
  }
2122
- const customContainer = this.getCustomPreRenderContainer();
2123
- customContainer?.removeEventListener('scroll', this.containerScrollListener);
2124
- this.containerScrollListener = null;
2141
+ this.resizeObserver?.disconnect();
2142
+ this.resizeObserver = new ResizeObserver(() => this.syncPreRenderStyle());
2143
+ this.resizeObserver.observe(placeholder);
2125
2144
  }
2126
2145
 
2127
2146
  /**
@@ -2131,7 +2150,7 @@ export class TsEmbed {
2131
2150
  public destroy(): void {
2132
2151
  try {
2133
2152
  this.removeFullscreenChangeHandler();
2134
- this.removeContainerScrollListener();
2153
+ this.resizeObserver?.disconnect();
2135
2154
  this.unsubscribeToEvents();
2136
2155
  this.preRenderWrapper?.remove();
2137
2156
  this.restorePreRenderContainerPosition();
@@ -2258,14 +2277,18 @@ export class TsEmbed {
2258
2277
 
2259
2278
  if (this.hostElement) {
2260
2279
  this.insertedDomEl = this.createPreRenderPlaceholder();
2261
- if ((this.viewConfig as { fullHeight: boolean }).fullHeight) {
2262
- // If fullHeight has already sized the wrapper, seed the
2263
- // placeholder with the same height so syncPreRenderStyle gets
2264
- // an accurate rect.
2265
- const existingHeight = this.preRenderWrapper.style.height;
2266
- if (existingHeight) {
2267
- (this.insertedDomEl as HTMLDivElement).style.height = existingHeight;
2268
- }
2280
+ // Carry a height fullHeight has already measured onto the fresh
2281
+ // placeholder, so a re-show does not flash at frameParams height.
2282
+ // UNMEASURED_WRAPPER_HEIGHT is what createPreRenderWrapper() seeds
2283
+ // before anything has been measured; treating that as a measurement
2284
+ // is what made a first reveal overshoot to a full viewport.
2285
+ const wrapperHeight = this.preRenderWrapper.style.height;
2286
+ if (
2287
+ (this.viewConfig as { fullHeight: boolean }).fullHeight
2288
+ && wrapperHeight
2289
+ && wrapperHeight !== UNMEASURED_WRAPPER_HEIGHT
2290
+ ) {
2291
+ (this.insertedDomEl as HTMLDivElement).style.height = wrapperHeight;
2269
2292
  }
2270
2293
 
2271
2294
  const placeHolderId = this.getPreRenderIds().placeHolder;
@@ -2282,32 +2305,20 @@ export class TsEmbed {
2282
2305
  this.hostElement.appendChild(this.insertedDomEl);
2283
2306
 
2284
2307
  this.syncPreRenderStyle();
2285
-
2286
- const customContainer = this.getCustomPreRenderContainer();
2287
- if (customContainer && !this.containerScrollListener) {
2288
- this.containerScrollListener = () => this.syncPreRenderStyle();
2289
- customContainer.addEventListener('scroll', this.containerScrollListener);
2290
- }
2291
-
2292
- if (!this.getPreRenderConfig().doNotTrackSize) {
2293
- const observeTarget = (this.insertedDomEl as HTMLElement) ?? this.hostElement;
2294
- this.resizeObserver = new ResizeObserver((entries) => {
2295
- entries.forEach((entry) => {
2296
- if (entry.target === observeTarget) {
2297
- this.syncPreRenderStyle();
2298
- }
2299
- });
2300
- });
2301
- this.resizeObserver.observe(observeTarget);
2302
- }
2308
+ this.observePreRenderSize();
2303
2309
  }
2304
2310
 
2305
2311
  removeStyleProperties(this.preRenderWrapper, [
2306
2312
  'z-index',
2307
2313
  'opacity',
2308
- 'pointer-events',
2309
2314
  'overflow',
2315
+ 'transform',
2310
2316
  ]);
2317
+ // Set rather than removed: a container that carries `pointer-events:
2318
+ // none` — the usual styling for a parking root that must not swallow
2319
+ // clicks — passes it down, and dropping the property here would leave
2320
+ // the inherited `none` in force and the frame dead to input.
2321
+ setStyleProperties(this.preRenderWrapper, { pointerEvents: 'auto' });
2311
2322
  this.subscribeToEvents();
2312
2323
 
2313
2324
  // Setup fullscreen change handler for prerendered components
@@ -2379,22 +2390,26 @@ export class TsEmbed {
2379
2390
  return;
2380
2391
  }
2381
2392
  const { zIndex } = this.getPreRenderConfig();
2393
+ // A hidden pre-render frame must add no scroll space to the host page.
2382
2394
  const preRenderHideStyles = {
2383
2395
  opacity: '0',
2384
2396
  pointerEvents: 'none',
2385
2397
  zIndex: zIndex !== undefined ? String(zIndex) : '-1000',
2386
- position: 'absolute',
2398
+ // Resolves to the viewport, so the hidden frame belongs to no
2399
+ // scroll container's overflow; syncPreRenderStyle restores absolute
2400
+ // on show.
2401
+ position: 'fixed',
2387
2402
  top: '0',
2388
2403
  left: '0',
2389
2404
  overflow: 'hidden',
2405
+ // The one exception: a transformed or contained ancestor captures
2406
+ // `fixed`; parking above the top edge keeps it out of that
2407
+ // overflow.
2408
+ transform: PRERENDER_PARKED_TRANSFORM,
2390
2409
  };
2391
2410
  setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
2392
2411
 
2393
- this.removeContainerScrollListener();
2394
-
2395
- if (this.resizeObserver) {
2396
- this.resizeObserver.disconnect();
2397
- }
2412
+ this.resizeObserver?.disconnect();
2398
2413
 
2399
2414
  const placeHolderEle = this.getPreRenderPlaceHolderElement();
2400
2415
  if (placeHolderEle) {
package/src/index.ts CHANGED
@@ -159,6 +159,7 @@ import {
159
159
  ScheduleEmailRequest,
160
160
  NavigateRequest,
161
161
  SetActiveTabRequest,
162
+ UpdateThemeRequest,
162
163
  } from './contracts/host-event-contracts';
163
164
  import {
164
165
  convertFilterChangedToUpdateFiltersPayload,
@@ -292,6 +293,7 @@ export {
292
293
  ScheduleEmailRequest,
293
294
  NavigateRequest,
294
295
  SetActiveTabRequest,
296
+ UpdateThemeRequest,
295
297
  LiveboardTab,
296
298
  LiveboardGroup,
297
299
  ApplicabilityLevel,
@@ -92,6 +92,7 @@ export {
92
92
  ScheduleEmailRequest,
93
93
  NavigateRequest,
94
94
  SetActiveTabRequest,
95
+ UpdateThemeRequest,
95
96
  LiveboardTab,
96
97
  LiveboardGroup,
97
98
  ApplicabilityLevel,