Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCAL-233969: Optimise info call (WIP) #66

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,16 @@ export const embedConfig: any = {
const originalWindow = window;

export const mockSessionInfoApiResponse = {
userGUID: '1234',
releaseVersion: 'test',
configInfo: {
isPublicUser: false,
mixpanelConfig: {
production: true,
devSdkKey: 'devKey',
prodSdkKey: 'prodKey',
info: {
userGUID: '1234',
releaseVersion: 'test',
configInfo: {
isPublicUser: false,
mixpanelConfig: {
production: true,
devSdkKey: 'devKey',
prodSdkKey: 'prodKey',
},
},
},
};
Expand Down
5 changes: 5 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
}

/**
* Enum for auth status emitted by the emitter returned from {@link init}.

Check warning on line 45 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

The type 'init' is undefined
* @group Authentication / Init
*/
export enum AuthStatus {
Expand All @@ -54,6 +54,11 @@
* Emits when the SDK authenticates successfully
*/
SDK_SUCCESS = 'SDK_SUCCESS',
/**
* @hidden
* Emits when the SDK authenticates successfully
*/
SESSION_INFO_SUCCESS = 'SESSION_INFO_SUCCESS',
/**
* Emits when the app sends an authentication success message
*/
Expand All @@ -66,13 +71,13 @@
* Emitted when inPopup is true in the SAMLRedirect flow and the
* popup is waiting to be triggered either programmatically
* or by the trigger button.
* @version SDK: 1.19.0

Check warning on line 74 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Invalid JSDoc @Version: "SDK: 1.19.0"
*/
WAITING_FOR_POPUP = 'WAITING_FOR_POPUP',
}

/**
* Event emitter returned from {@link init}.

Check warning on line 80 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

The type 'init' is undefined
* @group Authentication / Init
*/
export interface AuthEventEmitter {
Expand Down Expand Up @@ -100,7 +105,7 @@
once(event: AuthStatus.SUCCESS, listener: (sessionInfo: any) => void): this;
/**
* Trigger an event on the emitter returned from init.
* @param {@link AuthEvent}

Check warning on line 108 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Syntax error in type: @link AuthEvent
*/
emit(event: AuthEvent, ...args: any[]): boolean;
/**
Expand All @@ -119,7 +124,7 @@
}

/**
* Events which can be triggered on the emitter returned from {@link init}.

Check warning on line 127 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

The type 'init' is undefined
* @group Authentication / Init
*/
export enum AuthEvent {
Expand All @@ -132,7 +137,7 @@

let authEE: EventEmitter<AuthStatus | AuthEvent>;

/**

Check warning on line 140 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @returns declaration
*
*/
export function getAuthEE(): EventEmitter<AuthStatus | AuthEvent> {
Expand All @@ -141,7 +146,7 @@

/**
*
* @param eventEmitter

Check warning on line 149 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "eventEmitter" description

Check warning on line 149 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "eventEmitter" type
*/
export function setAuthEE(eventEmitter: EventEmitter<AuthStatus | AuthEvent>): void {
authEE = eventEmitter;
Expand Down Expand Up @@ -176,7 +181,7 @@

/**
*
* @param failureType

Check warning on line 184 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "failureType" description

Check warning on line 184 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "failureType" type
*/
export function notifyAuthFailure(failureType: AuthFailureType): void {
if (!authEE) {
Expand Down
6 changes: 6 additions & 0 deletions src/embed/ts-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
import { AuthFailureType } from '../auth';
import { getEmbedConfig } from './embedConfig';
import { ERROR_MESSAGE } from '../errors';
import { getSessionInfo } from '../utils/sessionInfoService';

const { version } = pkgInfo;

Expand Down Expand Up @@ -659,6 +660,11 @@ export class TsEmbed {
elHeight: this.iFrame.clientHeight,
timeTookToLoad: loadTimestamp - initTimestamp,
});
getSessionInfo().then((data) => {
if (data?.info) {
this.trigger(HostEvent.InfoSuccess, data);
}
});
});
this.iFrame.addEventListener('error', () => {
nextInQueue();
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3194,6 +3194,15 @@ export enum HostEvent {
* @version SDK: 1.29.0 | Thoughtspot: 10.1.0.cl
*/
GetParameters = 'GetParameters',
/**
* @hidden
* Notify when info call is completed successfully
* ```js
* liveboardEmbed.trigger(HostEvent.InfoSuccess, data);
*```
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
*/
InfoSuccess = 'InfoSuccess',
/**
* Triggers update of persoanlised view for a liveboard
* ```js
Expand Down
4 changes: 2 additions & 2 deletions src/utils/authService/authService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('Unit test for authService', () => {
const response = await fetchSessionInfoService(thoughtSpotHost);
expect(response.success).toBe(true);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toBeCalledWith(`${thoughtSpotHost}${EndPoints.SESSION_INFO}`, {
expect(fetch).toBeCalledWith(`${thoughtSpotHost}${EndPoints.PREAUTH_INFO}`, {
credentials: 'include',
});
});
Expand Down Expand Up @@ -116,7 +116,7 @@ describe('Unit test for authService', () => {
} catch (e) {
//
}
expect(logger.error).toHaveBeenCalledWith('Failed to fetch http://localhost:3000/callosum/v1/session/info', 'error');
expect(logger.error).toHaveBeenCalledWith('Failed to fetch http://localhost:3000/prism/preauth/info', 'error');

try {
await fetchBasicAuthService(thoughtSpotHost, username, password);
Expand Down
1 change: 1 addition & 0 deletions src/utils/authService/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { logger } from '../logger';
export const EndPoints = {
AUTH_VERIFICATION: '/callosum/v1/session/info',
SESSION_INFO: '/callosum/v1/session/info',
PREAUTH_INFO: '/prism/preauth/info',
SAML_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/saml/login?targetURLPath=${targetUrl}`,
OIDC_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/oidc/login?targetURLPath=${targetUrl}`,
TOKEN_LOGIN: '/callosum/v1/session/login/token',
Expand Down
125 changes: 123 additions & 2 deletions src/utils/authService/tokenizedAuthService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import * as tokenizedFetchModule from '../../tokenizedFetch';
import { isActiveService } from './tokenizedAuthService';
import { isActiveService, fetchSessionInfoService } from './tokenizedAuthService';
import { EndPoints } from './authService';
import { logger } from '../logger';

const thoughtspotHost = 'http://thoughtspotHost';

describe('tokenizedAuthService', () => {
test('isActiveService is fetch returns ok', async () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

test('isActiveService if fetch returns ok', async () => {
jest.spyOn(tokenizedFetchModule, 'tokenizedFetch').mockResolvedValueOnce({
ok: true,
});
Expand Down Expand Up @@ -34,3 +42,116 @@ describe('tokenizedAuthService', () => {
expect(logger.warn).toHaveBeenCalled();
});
});

describe('fetchSessionInfoService', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

test('fetchSessionInfoService should return a V2 info response containing the info key', async () => {
jest.spyOn(tokenizedFetchModule, 'tokenizedFetch').mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
json: async () => ({
info: {
configInfo: {
mixpanelConfig: {
devSdkKey: 'devSdkKey',
},
},
userGUID: 'userGUID',
},
}), // Mock JSON response
});

let sessionInfoResp;
try {
sessionInfoResp = await fetchSessionInfoService('http://thoughtspotHost');
} catch (e) {
//
}

// Check if the returned data contains the 'info' key
expect(sessionInfoResp).toHaveProperty('info');
});

it('should handle a 404 error from fetchPreauthInfoService and call fetchV1InfoService', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
message: 'Not Found',
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue('Not Found'),
})
// Mock for fetchV1InfoService
.mockResolvedValueOnce({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue({ data: 'mocked session info' }),
});

const result = await fetchSessionInfoService(thoughtspotHost);

expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockFetch).toHaveBeenNthCalledWith(1, `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
expect(mockFetch).toHaveBeenNthCalledWith(2, `${thoughtspotHost}${EndPoints.SESSION_INFO}`, {});
expect(result).toEqual({ data: 'mocked session info' });
});

it('should handle a error from both fetchPreauthInfoService and call fetchV1InfoService', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue('Not Found'),
})
// Mock for fetchV1InfoService
.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Something went wrong',
text: jest.fn().mockResolvedValue('Internal Server Error'),
json: jest.fn().mockResolvedValue({ data: 'mocked session info' }),
});

try {
await fetchSessionInfoService(thoughtspotHost);
} catch (e) {
expect(e.message).toContain('Failed to fetch session info: Something went wrong');
}

expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockFetch).toHaveBeenNthCalledWith(1, `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
expect(mockFetch).toHaveBeenNthCalledWith(2, `${thoughtspotHost}${EndPoints.SESSION_INFO}`, {});
});

it('should return an empty object if an error other than 404 occurs', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue('Internal Server Error'),
});

const result = await fetchSessionInfoService(thoughtspotHost);

expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(`${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
expect(result).toEqual({});
});
});
74 changes: 69 additions & 5 deletions src/utils/authService/tokenizedAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { EndPoints } from './authService';
function tokenizedFailureLoggedFetch(url: string, options: RequestInit = {}): Promise<Response> {
return tokenizedFetch(url, options).then(async (r) => {
if (!r.ok && r.type !== 'opaqueredirect' && r.type !== 'opaque') {
logger.error(`Failed to fetch ${url}`, await r.text?.());
if (r.status === 404) {
logger.warn(`Failed to fetch ${url}`, await r.text?.());
} else {
logger.error(`Failed to fetch ${url}`, await r.text?.());
}
}
return r;
});
Expand All @@ -25,14 +29,74 @@ function tokenizedFailureLoggedFetch(url: string, options: RequestInit = {}): Pr
* const response = await sessionInfoService();
* ```
*/
export async function fetchSessionInfoService(thoughtspotHost: string): Promise<any> {
export async function fetchPreauthInfoService(thoughtspotHost: string): Promise<any> {
const sessionInfoPath = `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`;
const response = await tokenizedFailureLoggedFetch(sessionInfoPath);
if (!response.ok) {
const error: any = new Error(`Failed to fetch auth info: ${response.statusText}`);
error.status = response.status; // Attach the status code to the error object
throw error;
}

return response;
}

/**
* Fetches the session info from the ThoughtSpot server.
* @param thoughtspotHost
* @returns {Promise<any>}
* @example
* ```js
* const response = await sessionInfoService();
* ```
*/
export async function fetchV1InfoService(thoughtspotHost: string): Promise<any> {
const sessionInfoPath = `${thoughtspotHost}${EndPoints.SESSION_INFO}`;
const response = await tokenizedFailureLoggedFetch(sessionInfoPath);
if (!response.ok) {
throw new Error(`Failed to fetch session info: ${response.statusText}`);
const error: any = new Error(`Failed to fetch session info: ${response.statusText}`);
error.status = response.status; // Attach the status code to the error object
throw error;
}

return response;
}

/**
* Fetches the session info from the ThoughtSpot server.
* @param thoughtspotHost
* @returns {Promise<any>}
* @example
* ```js
* const response = await sessionInfoService();
* ```
*/
export async function fetchSessionInfoService(thoughtspotHost: string): Promise<any> {
try {
const response = await fetchPreauthInfoService(thoughtspotHost);

// Convert Headers to a plain object
const headers: Record<string, string> = {};
response?.headers?.forEach((value: string, key: string) => {
headers[key] = value;
});
const data = await response.json();

return {
...data,
status: 200,
headers,
};
} catch (error) {
if (error.status === 404) {
const response = await fetchV1InfoService(thoughtspotHost);
const data = await response.json();

return data;
}

return {};
}
const data = await response.json();
return data;
}

/**
Expand Down
21 changes: 14 additions & 7 deletions src/utils/sessionInfoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,25 @@ export function getCachedSessionInfo(): SessionInfo | null {
* @version SDK: 1.28.3 | ThoughtSpot: *
*/
export const getSessionDetails = (sessionInfoResp: any): SessionInfo => {
const devMixpanelToken = sessionInfoResp.configInfo.mixpanelConfig.devSdkKey;
const prodMixpanelToken = sessionInfoResp.configInfo.mixpanelConfig.prodSdkKey;
const mixpanelToken = sessionInfoResp.configInfo.mixpanelConfig.production
const infoResp = sessionInfoResp?.info ?? sessionInfoResp;
let configInfo = sessionInfoResp?.info
? sessionInfoResp.info?.configInfo
: sessionInfoResp.configInfo;

configInfo = configInfo || {};

const devMixpanelToken = configInfo.mixpanelConfig?.devSdkKey;
const prodMixpanelToken = configInfo.mixpanelConfig?.prodSdkKey;
const mixpanelToken = configInfo.mixpanelConfig?.production
? prodMixpanelToken
: devMixpanelToken;
return {
userGUID: sessionInfoResp.userGUID,
userGUID: infoResp.userGUID,
mixpanelToken,
isPublicUser: sessionInfoResp.configInfo.isPublicUser,
isPublicUser: configInfo.isPublicUser,
releaseVersion: sessionInfoResp.releaseVersion,
clusterId: sessionInfoResp.configInfo.selfClusterId,
clusterName: sessionInfoResp.configInfo.selfClusterName,
clusterId: configInfo.selfClusterId,
clusterName: configInfo.selfClusterName,
...sessionInfoResp,
};
};
Expand Down
Loading