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

feat: gainsight PX destination (#1852) #1889

Merged
merged 12 commits into from
Oct 25, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const DIR_NAME = 'Gainsight_PX';
const NAME = 'GAINSIGHT_PX';
const DISPLAY_NAME = 'Gainsight PX';

Check warning on line 3 in packages/analytics-js-common/src/constants/integrations/Gainsight_PX/constants.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/integrations/Gainsight_PX/constants.ts#L1-L3

Added lines #L1 - L3 were not covered by tests

const DISPLAY_NAME_TO_DIR_NAME_MAP = { [DISPLAY_NAME]: DIR_NAME };
const CNameMapping = {

Check warning on line 6 in packages/analytics-js-common/src/constants/integrations/Gainsight_PX/constants.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/integrations/Gainsight_PX/constants.ts#L5-L6

Added lines #L5 - L6 were not covered by tests
[NAME]: NAME,
Gainsight_PX: NAME,
};

export { NAME, CNameMapping, DISPLAY_NAME_TO_DIR_NAME_MAP, DISPLAY_NAME, DIR_NAME };

Check warning on line 11 in packages/analytics-js-common/src/constants/integrations/Gainsight_PX/constants.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/integrations/Gainsight_PX/constants.ts#L11

Added line #L11 was not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const clientToServerNames = {
COMMANDBAR: 'CommandBar',
NINETAILED: 'Ninetailed',
XPIXEL: 'XPixel',
GAINSIGHT_PX: 'Gainsight_PX',
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved
};

export { clientToServerNames };
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const configToIntNames = {
COMMANDBAR: 'CommandBar',
NINETAILED: 'Ninetailed',
XPIXEL: 'XPixel',
GAINSIGHT_PX: 'Gainsight_PX',
};

export { configToIntNames };
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ import {
CommandBarDirectoryName,
NinetailedDisplayName,
NinetailedDirectoryName,
Gainsight_PXDisplayName,
Gainsight_PXDirectoryName,
} from './destinationNames';

// The destination directory name is used as the destination SDK file name in CDN
Expand Down Expand Up @@ -241,6 +243,7 @@ const destDisplayNamesToFileNamesMap: Record<string, string> = {
[SpotifyPixelDisplayName]: SpotifyPixelDirectoryName,
[CommandBarDisplayName]: CommandBarDirectoryName,
[NinetailedDisplayName]: NinetailedDirectoryName,
[Gainsight_PXDisplayName]: Gainsight_PXDirectoryName,
};

export { destDisplayNamesToFileNamesMap };
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,7 @@
DISPLAY_NAME as NinetailedDisplayName,
DIR_NAME as NinetailedDirectoryName,
} from './Ninetailed/constants';
export {
DISPLAY_NAME as Gainsight_PXDisplayName,
DIR_NAME as Gainsight_PXDirectoryName,

Check warning on line 295 in packages/analytics-js-common/src/constants/integrations/destinationNames.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/integrations/destinationNames.ts#L293-L295

Added lines #L293 - L295 were not covered by tests
} from './Gainsight_PX/constants';
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import { CNameMapping as CommandBar } from './CommandBar/constants';
import { CNameMapping as Ninetailed } from './Ninetailed/constants';
import { CNameMapping as XPixel } from './XPixel/constants';
import { CNameMapping as Gainsight_PX } from './Gainsight_PX/constants';

Check warning on line 81 in packages/analytics-js-common/src/constants/integrations/integration_cname.js

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/integrations/integration_cname.js#L81

Added line #L81 was not covered by tests
// for sdk side native integration identification
// add a mapping from common names to index.js exported key names as identified by Rudder
const commonNames = {
Expand Down Expand Up @@ -162,6 +163,7 @@
...Sprig,
...SpotifyPixel,
...XPixel,
...Gainsight_PX
};

export { commonNames };
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import Gainsight_PX from '../../../src/integrations/Gainsight_PX/browser';
import { loadNativeSdk } from '../../../src/integrations/Gainsight_PX/nativeSdkLoader';
import { getDestinationOptions } from '../../../src/integrations/Gainsight_PX/utils';

// Mock the external dependencies
jest.mock('../../../src/integrations/Gainsight_PX/nativeSdkLoader');
jest.mock('../../../src/integrations/Gainsight_PX/utils');

describe('Gainsight_PX', () => {
let gainsightPX;
let mockAnalytics;
let mockConfig;

beforeEach(() => {
// Reset mocks
jest.clearAllMocks();

// Mock window.aptrinsic
window.aptrinsic = jest.fn();

// Mock analytics object
mockAnalytics = {
logLevel: 'debug',
getUserId: jest.fn(),
getUserTraits: jest.fn(),
getGroupId: jest.fn(),
getGroupTraits: jest.fn(),
loadOnlyIntegrations: {},
};

// Mock config
mockConfig = {
productTagKey: 'test-product-key',
dataCenter: 'US',
};

gainsightPX = new Gainsight_PX(mockConfig, mockAnalytics);
});

describe('init', () => {
it('should load native SDK and initialize', () => {
getDestinationOptions.mockReturnValue({ someOption: 'value' });
mockAnalytics.getUserId.mockReturnValue('test-user-id');
mockAnalytics.getUserTraits.mockReturnValue({ name: 'Test User' });
mockAnalytics.getGroupId.mockReturnValue('test-group-id');
mockAnalytics.getGroupTraits.mockReturnValue({ plan: 'premium' });

gainsightPX.init();

expect(loadNativeSdk).toHaveBeenCalledWith('test-product-key', 'US', { someOption: 'value' });
expect(window.aptrinsic).toHaveBeenCalledWith(
'identify',
{ id: 'test-user-id', name: 'Test User' },
{ id: 'test-group-id', plan: 'premium' }
);
});

it('should not call identify if user ID is not present', () => {
mockAnalytics.getUserId.mockReturnValue(null);

gainsightPX.init();

expect(loadNativeSdk).toHaveBeenCalled();
expect(window.aptrinsic).not.toHaveBeenCalled();
});
});

describe('identify', () => {
it('should call aptrinsic identify with user data', () => {
const rudderElement = {
message: {
userId: 'test-user-id',
context: {
traits: { name: 'Test User', email: '[email protected]' },
},
},
};

gainsightPX.identify(rudderElement);

expect(window.aptrinsic).toHaveBeenCalledWith(
'identify',
{ id: 'test-user-id', name: 'Test User', email: '[email protected]' },
{}
);
});

it('should not call aptrinsic identify if userId is missing', () => {
const rudderElement = {
message: {
context: {
traits: { name: 'Test User' },
},
},
};

gainsightPX.identify(rudderElement);

expect(window.aptrinsic).not.toHaveBeenCalled();
});
});

describe('group', () => {
it('should call aptrinsic identify with group data', () => {
const rudderElement = {
message: {
userId: 'test-user-id',
groupId: 'test-group-id',
traits: { plan: 'premium' },
context: {
traits: { name: 'Test User' },
},
},
};

gainsightPX.group(rudderElement);

expect(window.aptrinsic).toHaveBeenCalledWith(
'identify',
{ id: 'test-user-id', name: 'Test User' },
{ id: 'test-group-id', plan: 'premium' }
);
});

it('should use anonymousId if groupId is not present', () => {
const rudderElement = {
message: {
anonymousId: 'anon-id',
traits: { plan: 'basic' },
},
};

gainsightPX.group(rudderElement);

expect(window.aptrinsic).toHaveBeenCalledWith(
'identify',
{},
{ id: 'anon-id', plan: 'basic' }
);
});
});

describe('track', () => {
it('should call aptrinsic track with event data', () => {
const rudderElement = {
message: {
event: 'Test Event',
properties: { category: 'test', value: 10 },
},
};

gainsightPX.track(rudderElement);

expect(window.aptrinsic).toHaveBeenCalledWith(
'track',
'Test Event',
{ category: 'test', value: 10 }
);
});

it('should not call aptrinsic track if event name is missing', () => {
const rudderElement = {
message: {
properties: { category: 'test' },
},
};

gainsightPX.track(rudderElement);

expect(window.aptrinsic).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable lines-between-class-members */
import {
NAME,
DISPLAY_NAME,
} from '@rudderstack/analytics-js-common/constants/integrations/Gainsight_PX/constants';
import Logger from '../../utils/logger';
import { loadNativeSdk } from './nativeSdkLoader';
import { getDestinationOptions } from './utils';

const logger = new Logger(DISPLAY_NAME);

class Gainsight_PX {
constructor(config, analytics, destinationInfo) {
if (analytics.logLevel) {
logger.setLogLevel(analytics.logLevel);
}
this.analytics = analytics;
this.productKey = config.productTagKey;
this.dataCenter = !config.dataCenter ? 'US' : config.dataCenter;
this.name = NAME;
({
shouldApplyDeviceModeTransformation: this.shouldApplyDeviceModeTransformation,
propagateEventsUntransformedOnError: this.propagateEventsUntransformedOnError,
destinationId: this.destinationId,
} = destinationInfo ?? {});
}

init() {
const pxConfig = getDestinationOptions(this.analytics.loadOnlyIntegrations) || {};
loadNativeSdk(this.productKey, this.dataCenter, pxConfig);
this.initializeMe();
}

initializeMe() {
const userId = this.analytics.getUserId();

// Only proceed with identify if user ID is defined
if (userId) {
const visitorObj = { id: userId, ...this.analytics.getUserTraits() };

const accountObj = {
id: this.analytics.getGroupId(),
...this.analytics.getGroupTraits(),
};

window.aptrinsic('identify', visitorObj, accountObj);
}
}

isLoaded() {
return !!(window.aptrinsic && window.aptrinsic.init);
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved
}

isReady() {
return this.isLoaded();

Check warning on line 56 in packages/analytics-js-integrations/src/integrations/Gainsight_PX/browser.js

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-integrations/src/integrations/Gainsight_PX/browser.js#L56

Added line #L56 was not covered by tests
}
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved

/* utility functions --- Ends here --- */

/*
* Gainsight_PX MAPPED FUNCTIONS :: identify, track, group
*/

identify(rudderElement) {
let visitorObj = {};
const accountObj = {};
const { userId, context } = rudderElement.message;
const id = userId;
const userTraits = context?.traits || {};
visitorObj = {
id,
...userTraits,
};
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved

if (!userId) {
return;
}
window.aptrinsic('identify', visitorObj, accountObj);
}

/*
*Group call maps to an account for which visitor belongs.
*It is same as identify call
*/
group(rudderElement) {
let accountObj = {};
let visitorObj = {};
const { userId, traits, context, groupId, anonymousId } = rudderElement.message;
accountObj.id = groupId || anonymousId;
accountObj = {
...accountObj,
...traits,
};
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved

const userTraits = context?.traits || {};
if (userId) {
visitorObj = {
id: userId,
...userTraits,
};
}

window.aptrinsic('identify',visitorObj, accountObj);
}
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved

// Custom Events
track(rudderElement) {
const { event, properties } = rudderElement.message;
if (!event) {
logger.error('Cannot send un-named custom event');
return;
}
const props = properties;
window.aptrinsic('track', event, props);
}
}

export default Gainsight_PX;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Gainsight_PX } from './browser';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { LOAD_ORIGIN } from '@rudderstack/analytics-js-common/v1.1/utils/constants';

function loadNativeSdk(productKey, dataCenter, pxConfig) {
let hostName = 'web-sdk.aptrinsic.com';
switch (dataCenter) {
case 'EU':
hostName = 'web-sdk-eu.aptrinsic.com';
break;
case 'US2':
hostName = 'web-sdk-us2.aptrinsic.com';
break;
}
const sdkUrl= "https://" + hostName + "/api/aptrinsic.js";
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved

(function(n,t,a,e, co){
var i="aptrinsic";
n[i]=n[i]||function(){
(n[i].q=n[i].q||[]).push(arguments)
shrouti1507 marked this conversation as resolved.
Show resolved Hide resolved
},n[i].p=e;
n[i].c=co;
var r=t.createElement("script");
r.async=!0,r.src=a+"?a="+e;
r.setAttribute('data-loader', LOAD_ORIGIN);
var c=t.getElementsByTagName("script")[0];
c.parentNode.insertBefore(r,c)
})(window, document, sdkUrl, productKey, pxConfig);
}

export { loadNativeSdk };
Loading