From 9930d3f8a425070617cc7073baa46b992dc9f2a2 Mon Sep 17 00:00:00 2001 From: Shikhar Bhargava Date: Thu, 30 Jul 2026 19:19:54 -0400 Subject: [PATCH] SCAL-326039 Add versioning for spotter embed and a feature matrix --- src/embed/conversation.spec.ts | 170 ++++++++++++++++++++++++++++++++- src/embed/conversation.ts | 97 ++++++++++++++++++- src/embed/spotter-utils.ts | 82 +++++++++++++++- src/index.ts | 3 +- src/react/all-types-export.ts | 1 + src/types.ts | 1 + 6 files changed, 350 insertions(+), 4 deletions(-) diff --git a/src/embed/conversation.spec.ts b/src/embed/conversation.spec.ts index f89a251bb..e2734cd98 100644 --- a/src/embed/conversation.spec.ts +++ b/src/embed/conversation.spec.ts @@ -1,8 +1,10 @@ -import { SpotterEmbed, SpotterEmbedViewConfig, SpotterQueryMode, ConversationEmbed } from './conversation'; +import { SpotterEmbed, SpotterEmbedViewConfig, SpotterQueryMode, SpotterVersion, ConversationEmbed } from './conversation'; +import { SPOTTER_OPTION_VERSION_SUPPORT } from './spotter-utils'; import { TsEmbed } from './ts-embed'; import * as authInstance from '../auth'; import { Action, init } from '../index'; import { AuthType, Param, RuntimeFilterOp, ErrorDetailsTypes, EmbedErrorCodes, EmbedEvent } from '../types'; +import { logger } from '../utils/logger'; import { getDocumentBody, getIFrameSrc, @@ -557,6 +559,172 @@ describe('ConversationEmbed', () => { ); }); + describe('spotterVersion', () => { + afterEach(() => { + jest.restoreAllMocks(); + SPOTTER_OPTION_VERSION_SUPPORT.disableSourceSelection = 'unconfirmed'; + }); + + it('should not include a spotterVersion param when unset (default Spotter3 behavior)', async () => { + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + + it('should include spotterVersion=spotterX when SpotterX is selected', async () => { + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.SpotterX, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true&spotterVersion=spotterX#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + + it('should include spotterVersion=spotter3 when V3 (Spotter3 alias) is selected', async () => { + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.V3, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true&spotterVersion=spotter3#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + + it('should include spotterVersion=spotterX when V4 (SpotterX alias) is selected', async () => { + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.V4, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true&spotterVersion=spotterX#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + + it('should not warn when spotterVersion is V3 (Spotter3 alias) regardless of options set', async () => { + SPOTTER_OPTION_VERSION_SUPPORT.disableSourceSelection = 'unsupported'; + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined); + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.V3, + disableSourceSelection: true, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should warn for an unsupported option when spotterVersion is V4 (SpotterX alias)', async () => { + SPOTTER_OPTION_VERSION_SUPPORT.disableSourceSelection = 'unsupported'; + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined); + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.V4, + disableSourceSelection: true, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it('should include spotterVersion=latest when Latest is selected', async () => { + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.Latest, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true&spotterVersion=latest#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + + it('should include spotterVersion=spotter3 when explicitly selected', async () => { + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.Spotter3, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true&spotterVersion=spotter3#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + + it('should not warn for options that are only unconfirmed (not confirmed unsupported) on SpotterX', async () => { + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined); + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.SpotterX, + disableSourceSelection: true, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should not warn when spotterVersion is Spotter3 regardless of options set', async () => { + SPOTTER_OPTION_VERSION_SUPPORT.disableSourceSelection = 'unsupported'; + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined); + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.Spotter3, + disableSourceSelection: true, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should warn once for an option flagged unsupported for the selected version, without altering the URL', async () => { + SPOTTER_OPTION_VERSION_SUPPORT.disableSourceSelection = 'unsupported'; + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined); + const viewConfig: SpotterEmbedViewConfig = { + worksheetId: 'worksheetId', + searchOptions: { searchQuery: 'searchQuery' }, + spotterVersion: SpotterVersion.SpotterX, + disableSourceSelection: true, + }; + const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig); + await conversationEmbed.render(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('disableSourceSelection'), + ); + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/v2/?${defaultParams}&isSpotterExperienceEnabled=true&disableSourceSelection=true&spotterVersion=spotterX#/embed/insights/conv-assist?worksheet=worksheetId&query=searchQuery`, + ); + }); + }); + describe('spotter chat hiddenActions', () => { it.each([ ['SpotterChatConnectorResources', Action.SpotterChatConnectorResources], diff --git a/src/embed/conversation.ts b/src/embed/conversation.ts index 88233c812..19d64cfef 100644 --- a/src/embed/conversation.ts +++ b/src/embed/conversation.ts @@ -1,7 +1,7 @@ import { ERROR_MESSAGE } from '../errors'; import { Param, BaseViewConfig, RuntimeFilter, RuntimeParameter, ErrorDetailsTypes, EmbedErrorCodes, DefaultAppInitData, VisualizationOverrides, SpotterFileUploadFileTypes } from '../types'; import { TsEmbed } from './ts-embed'; -import { buildSpotterSidebarAppInitData, buildSpotterShareConversationAppInitData } from './spotter-utils'; +import { buildSpotterSidebarAppInitData, buildSpotterShareConversationAppInitData, warnUnsupportedSpotterVersionOptions } from './spotter-utils'; import { getQueryParamString, getFilterQuery, getRuntimeParameters, setParamIfDefined } from '../utils'; /** @@ -23,6 +23,36 @@ export enum SpotterQueryMode { RESEARCH = 'research', } +/** + * The Spotter experience version to render in `SpotterEmbed`. + * + * `Spotter3` is the current, default experience — omitting `spotterVersion` + * entirely behaves exactly as before this option existed. `SpotterX` opts + * into the next-generation Spotter experience. `V3`/`V4` are equivalent + * aliases for `Spotter3`/`SpotterX` respectively (same underlying version, + * just named after the release generation instead of the product name) — + * use whichever naming reads more clearly in your integration. `Latest` + * always resolves to whichever version the connected ThoughtSpot cluster + * currently considers newest, so it may change behavior across cluster + * upgrades without an SDK change. + * + * Not every `SpotterEmbedViewConfig` option is confirmed to behave the same + * way across versions yet — see {@link SPOTTER_OPTION_VERSION_SUPPORT} in + * `spotter-utils.ts` for current per-option support status. + * @version SDK: 1.53.0 | ThoughtSpot Cloud: TBD + */ +export enum SpotterVersion { + Spotter3 = 'spotter3', + /** Alias for {@link SpotterVersion.Spotter3}. */ + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + V3 = 'spotter3', + SpotterX = 'spotterX', + /** Alias for {@link SpotterVersion.SpotterX}. */ + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + V4 = 'spotterX', + Latest = 'latest', +} + /** * Configuration for the pin/unpin conversation feature in the Spotter sidebar. * Grouped into one object because pin exposes several related settings @@ -239,6 +269,9 @@ export interface SpotterChatViewConfig { * cards. The branding label prefix is controlled * separately via `toolResponseCardBrandingLabel`. * External MCP tool branding is not affected. + * + * Spotter version support: see {@link SPOTTER_OPTION_VERSION_SUPPORT} + * for current per-version status. * @default false */ hideToolResponseCardBranding?: boolean; @@ -248,12 +281,17 @@ export interface SpotterChatViewConfig { * `''` to hide the prefix entirely. Works * independently of `hideToolResponseCardBranding`. * External MCP tool branding is not affected. + * + * Spotter version support: see {@link SPOTTER_OPTION_VERSION_SUPPORT} + * for current per-version status. */ toolResponseCardBrandingLabel?: string; /** * Enables file upload in the Spotter chat interface. * * Supported embed types: `SpotterEmbed`, `LiveboardEmbed`, `AppEmbed` + * Spotter version support: see {@link SPOTTER_OPTION_VERSION_SUPPORT} + * for current per-version status. * @version SDK: 1.49.0 | ThoughtSpot: 26.6.0.cl * @default false */ @@ -262,6 +300,8 @@ export interface SpotterChatViewConfig { * Restricts the allowed file types for Spotter file upload. * * Supported embed types: `SpotterEmbed`, `LiveboardEmbed`, `AppEmbed` + * Spotter version support: see {@link SPOTTER_OPTION_VERSION_SUPPORT} + * for current per-version status. * @version SDK: 1.49.0 | ThoughtSpot: 26.6.0.cl */ spotterFileUploadFileTypes?: SpotterFileUploadFileTypes; @@ -269,6 +309,8 @@ export interface SpotterChatViewConfig { * Enables starter prompts in the Spotter chat interface. * * Supported embed types: SpotterEmbed, LiveboardEmbed, AppEmbed + * Spotter version support: see {@link SPOTTER_OPTION_VERSION_SUPPORT} + * for current per-version status. * @version SDK: 1.51.0 | ThoughtSpot: 26.8.0.cl * @default false */ @@ -294,6 +336,8 @@ export interface SpotterEmbedViewConfig extends Omit = { + disableSourceSelection: 'unconfirmed', + hideSourceSelection: 'unconfirmed', + showSpotterLimitations: 'unconfirmed', + hideSampleQuestions: 'unconfirmed', + runtimeFilters: 'unconfirmed', + runtimeParameters: 'unconfirmed', + updatedSpotterChatPrompt: 'unconfirmed', + defaultQueryMode: 'unconfirmed', + enableStopAnswerGenerationEmbed: 'unconfirmed', + spotterSidebarConfig: 'unconfirmed', + 'spotterChatConfig.hideToolResponseCardBranding': 'unconfirmed', + 'spotterChatConfig.toolResponseCardBrandingLabel': 'unconfirmed', + 'spotterChatConfig.spotterFileUploadEnabled': 'unconfirmed', + 'spotterChatConfig.spotterFileUploadFileTypes': 'unconfirmed', + 'spotterChatConfig.enableStarterPrompts': 'unconfirmed', + spotterShareConversationConfig: 'unconfirmed', + sharedConversationId: 'unconfirmed', +}; + +const getByPath = (obj: Record, path: string): unknown => path + .split('.') + .reduce( + (value, key) => (value && typeof value === 'object' ? (value as Record)[key] : undefined), + obj, + ); + +/** + * Logs a non-fatal warning for every `SpotterEmbedViewConfig` option that is + * set and marked `'unsupported'` for the embed's chosen `spotterVersion`. + * No-op for `Spotter3` (the baseline) and for options marked `'supported'` + * or `'unconfirmed'` — this only flags known-bad combinations, so it never + * spams users about options that simply haven't been verified yet. + * + * Advisory only: never throws and never changes the resolved config or the + * generated iframe URL. + */ +export function warnUnsupportedSpotterVersionOptions(viewConfig: SpotterEmbedViewConfig): void { + const { spotterVersion } = viewConfig; + // Compares against the literal 'spotter3' (SpotterVersion.Spotter3's value) + // rather than importing the enum, to avoid a circular import with + // conversation.ts (which imports this file). + if (!spotterVersion || spotterVersion === 'spotter3') return; + + Object.entries(SPOTTER_OPTION_VERSION_SUPPORT).forEach(([path, status]) => { + if (status !== 'unsupported') return; + const value = getByPath(viewConfig as unknown as Record, path); + if (value !== undefined) { + logger.warn( + `SpotterEmbed: option "${path}" is not supported for spotterVersion ` + + `"${spotterVersion}". It may be ignored or behave unexpectedly.`, + ); + } + }); +} diff --git a/src/index.ts b/src/index.ts index 8e7cc8ba2..f885d96e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,7 @@ import { PinboardEmbed, LiveboardViewConfig, LiveboardEmbed } from './embed/live import { SearchEmbed, SearchViewConfig } from './embed/search'; import { SearchBarEmbed, SearchBarViewConfig } from './embed/search-bar'; import { SpotterAgentEmbed, SpotterAgentEmbedViewConfig, BodylessConversation, BodylessConversationViewConfig} from './embed/bodyless-conversation'; -import { SpotterEmbed, SpotterEmbedViewConfig, SpotterChatViewConfig, SpotterSidebarViewConfig, SpotterQueryMode, SpotterShareConversationConfig, ConversationEmbed, ConversationViewConfig } from './embed/conversation'; +import { SpotterEmbed, SpotterEmbedViewConfig, SpotterChatViewConfig, SpotterSidebarViewConfig, SpotterQueryMode, SpotterVersion, SpotterShareConversationConfig, ConversationEmbed, ConversationViewConfig } from './embed/conversation'; import { SpotterVizConfig, SpotterVizStarterPrompt, SpotterVizLoaderTip } from './embed/spotter-viz-utils'; import { AuthFailureType, AuthStatus, AuthEvent, AuthEventEmitter, @@ -118,6 +118,7 @@ export { SpotterChatViewConfig, SpotterSidebarViewConfig, SpotterQueryMode, + SpotterVersion, SpotterShareConversationConfig, ConversationViewConfig, ConversationEmbed, diff --git a/src/react/all-types-export.ts b/src/react/all-types-export.ts index 1d811a105..81a6e13b1 100644 --- a/src/react/all-types-export.ts +++ b/src/react/all-types-export.ts @@ -60,4 +60,5 @@ export { InterceptedApiType, CustomActionsPosition, CustomActionTarget, + SpotterVersion, } from '../index'; diff --git a/src/types.ts b/src/types.ts index 42c3d434e..e95e9faf5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6669,6 +6669,7 @@ export enum Param { ShowSpotterRadiance = 'showSpotterRadiance', DefaultQueryMode = 'defaultQueryMode', EnableStopAnswerGenerationEmbed = 'enableStopAnswerGenerationEmbed', + SpotterVersion = 'spotterVersion', SpotterSidebarTitle = 'spotterSidebarTitle', SpotterSidebarDefaultExpanded = 'spotterSidebarDefaultExpanded', SpotterChatRenameLabel = 'spotterChatRenameLabel',