diff --git a/packages/analytics-js-integrations/__tests__/integrations/Wingify/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/Wingify/browser.test.js new file mode 100644 index 000000000..89d028098 --- /dev/null +++ b/packages/analytics-js-integrations/__tests__/integrations/Wingify/browser.test.js @@ -0,0 +1,207 @@ +import Wingify from '../../../src/integrations/Wingify/browser'; +import { integrations } from '../../../src/integrations/index'; + +const destinationInfo = { + areTransformationsConnected: false, + destinationId: 'sample-destination-id', +}; + +const mockWingify = new Wingify( + { + accountId: '654331', + settingsTolerance: 2000, + libraryTolerance: 2500, + isSPA: 1, + useExistingJquery: false, + sendExperimentTrack: false, + sendExperimentIdentify: false, + }, + { loglevel: 'debug', loadOnlyIntegrations: { Wingify: { loadIntegration: true } } }, + destinationInfo, +); + +describe('Wingify registry', () => { + test('should be registered in integrations map', () => { + expect(integrations.WINGIFY).toBeDefined(); + expect(typeof integrations.WINGIFY).toBe('function'); + }); +}); + +describe('Wingify init tests', () => { + let wingify; + + test('Testing init call of Wingify', () => { + const appendedNodes = []; + const appendChildSpy = jest + .spyOn(document.head, 'appendChild') + .mockImplementation(node => { + appendedNodes.push(node); + return node; + }); + + wingify = new Wingify( + { + accountId: '654331', + settingsTolerance: 2000, + libraryTolerance: 2500, + isSPA: 1, + useExistingJquery: false, + sendExperimentTrack: false, + sendExperimentIdentify: false, + }, + { loglevel: 'debug', loadOnlyIntegrations: { Wingify: { loadIntegration: true } } }, + destinationInfo, + ); + wingify.init(); + + const script = appendedNodes.find( + node => node.tagName === 'SCRIPT' && node.src === 'https://edge.wingify.net/tag/654331.js', + ); + expect(script).toBeDefined(); + + appendChildSpy.mockRestore(); + }); +}); + +describe('Wingify Track Event', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.spyOn(mockWingify, 'init').mockImplementation(() => { + window.WINGIFY = { + push: jest.fn(), + event: jest.fn(), + }; + return Promise.resolve(window.WINGIFY); + }); + }); + + test('Track call without parameters', async () => { + mockWingify.init(); + mockWingify.track({ + message: { + context: {}, + event: 'buttonClicked', + }, + }); + expect(window.WINGIFY.event).toHaveBeenCalled(); + expect(window.WINGIFY.event).toHaveBeenCalledWith( + 'rudder.buttonClicked', + {}, + { + ogName: 'buttonClicked', + source: 'rudderstack', + }, + ); + }); + + test('Track call with parameters', async () => { + mockWingify.init(); + mockWingify.track({ + message: { + context: {}, + event: 'checkoutCompleted', + properties: { + category: 'Food', + currency: 'INR', + total: 123, + }, + }, + }); + expect(window.WINGIFY.event).toHaveBeenCalled(); + expect(window.WINGIFY.event).toHaveBeenCalledWith( + 'rudder.checkoutCompleted', + { + category: 'Food', + currency: 'INR', + total: 123, + }, + { + ogName: 'checkoutCompleted', + source: 'rudderstack', + }, + ); + }); +}); + +describe('Wingify Identify Event', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.spyOn(mockWingify, 'init').mockImplementation(() => { + window.WINGIFY = { + push: jest.fn(), + visitor: jest.fn(), + }; + return Promise.resolve(window.WINGIFY); + }); + }); + + test('Visitor call with attributes', async () => { + mockWingify.init(); + mockWingify.identify({ + message: { + userId: 'rudder01', + context: { + traits: { + email: 'abc@rudderstack.com', + isRudderEvents: true, + }, + }, + }, + }); + expect(window.WINGIFY.visitor).toHaveBeenCalled(); + expect(window.WINGIFY.visitor).toHaveBeenCalledWith( + { + 'rudder.email': 'abc@rudderstack.com', + 'rudder.isRudderEvents': true, + }, + { + source: 'rudderstack', + }, + ); + }); + + test('Visitor call with top-level traits when context has no traits', async () => { + mockWingify.init(); + mockWingify.identify({ + message: { + userId: 'rudder01', + context: {}, + traits: { + plan: 'enterprise', + }, + }, + }); + expect(window.WINGIFY.visitor).toHaveBeenCalledWith( + { + 'rudder.plan': 'enterprise', + }, + { + source: 'rudderstack', + }, + ); + }); +}); + +describe('Wingify Track Event validation', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.spyOn(mockWingify, 'init').mockImplementation(() => { + window.WINGIFY = { + push: jest.fn(), + event: jest.fn(), + }; + return Promise.resolve(window.WINGIFY); + }); + }); + + test('Track call with whitespace-only event name is rejected', () => { + mockWingify.init(); + mockWingify.track({ + message: { + context: {}, + event: ' ', + }, + }); + expect(window.WINGIFY.event).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/analytics-js-integrations/__tests__/integrations/Wingify/nativeSdkLoader.test.js b/packages/analytics-js-integrations/__tests__/integrations/Wingify/nativeSdkLoader.test.js new file mode 100644 index 000000000..05c046a5f --- /dev/null +++ b/packages/analytics-js-integrations/__tests__/integrations/Wingify/nativeSdkLoader.test.js @@ -0,0 +1,57 @@ +import { loadNativeSdk } from '../../../src/integrations/Wingify/nativeSdkLoader'; + +describe('Wingify loadNativeSdk', () => { + const originalDocumentUrl = document.URL; + const originalGetEntriesByName = performance.getEntriesByName; + + beforeEach(() => { + delete window._wingify_code; + document.head.innerHTML = ''; + Object.defineProperty(document, 'URL', { + configurable: true, + value: 'https://www.test-host.com', + }); + performance.getEntriesByName = jest.fn().mockReturnValue([]); + }); + + afterEach(() => { + delete window._wingify_code; + document.head.innerHTML = ''; + Object.defineProperty(document, 'URL', { + configurable: true, + value: originalDocumentUrl, + }); + performance.getEntriesByName = originalGetEntriesByName; + }); + + test('should short-circuit when __wingify_disable__ is present in the URL', () => { + Object.defineProperty(document, 'URL', { + configurable: true, + value: 'https://www.test-host.com?__wingify_disable__', + }); + + loadNativeSdk('654331', 2000); + + expect(window._wingify_code).toBeUndefined(); + }); + + test('should not inject hide style when first-contentful-paint is available', () => { + performance.getEntriesByName = jest + .fn() + .mockReturnValue([{ name: 'first-contentful-paint' }]); + + loadNativeSdk('654331', 2000); + + expect(document.getElementById('_vis_opt_path_hides')).toBeNull(); + expect(window._wingify_code).toBeDefined(); + }); + + test('should inject hide style when first-contentful-paint is not available', () => { + loadNativeSdk('654331', 2000); + + const hideStyle = document.getElementById('_vis_opt_path_hides'); + expect(hideStyle).not.toBeNull(); + expect(hideStyle.textContent).toContain('body{'); + expect(window._wingify_code).toBeDefined(); + }); +}); diff --git a/packages/analytics-js-integrations/__tests__/integrations/Wingify/utils.test.js b/packages/analytics-js-integrations/__tests__/integrations/Wingify/utils.test.js new file mode 100644 index 000000000..c41e13100 --- /dev/null +++ b/packages/analytics-js-integrations/__tests__/integrations/Wingify/utils.test.js @@ -0,0 +1,36 @@ +import { sanitizeName, sanitizeAttributes } from '../../../src/integrations/Wingify/utils'; + +describe('Wingify utilities tests', () => { + describe('Sanitize Names', () => { + it('should trim and add the rudder prefix to the event name', () => { + const name1 = 'abcd'; + const name2 = ' abcd '; + + const expectedName = 'rudder.abcd'; + + const result1 = sanitizeName(name1); + const result2 = sanitizeName(name2); + + expect(result1).toEqual(expectedName); + expect(result2).toEqual(expectedName); + }); + }); + + describe('Sanitize Attributes', () => { + it('should sanitize all the keys of the traits object', () => { + const attributes = { + companySize: 100, + companyName: 'RudderStack', + }; + + const expectedAttributes = { + 'rudder.companySize': 100, + 'rudder.companyName': 'RudderStack', + }; + + const result = sanitizeAttributes(attributes); + + expect(result).toEqual(expectedAttributes); + }); + }); +}); diff --git a/packages/analytics-js-integrations/src/constants/Destinations.ts b/packages/analytics-js-integrations/src/constants/Destinations.ts index 55ce5a6df..ffe82f318 100644 --- a/packages/analytics-js-integrations/src/constants/Destinations.ts +++ b/packages/analytics-js-integrations/src/constants/Destinations.ts @@ -165,6 +165,8 @@ export const VERO_NAME = 'VERO'; export const VERO_DISPLAY_NAME = 'Vero'; export const VWO_NAME = 'VWO'; export const VWO_DISPLAY_NAME = 'VWO'; +export const WINGIFY_NAME = 'WINGIFY'; +export const WINGIFY_DISPLAY_NAME = 'Wingify'; export const WOOPRA_NAME = 'WOOPRA'; export const WOOPRA_DISPLAY_NAME = 'WOOPRA'; export const XPIXEL_NAME = 'XPIXEL'; diff --git a/packages/analytics-js-integrations/src/integrations/Wingify/browser.js b/packages/analytics-js-integrations/src/integrations/Wingify/browser.js new file mode 100644 index 000000000..58e9a1874 --- /dev/null +++ b/packages/analytics-js-integrations/src/integrations/Wingify/browser.js @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable no-undef */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable class-methods-use-this */ +/* eslint-disable camelcase */ +import { NAME, DISPLAY_NAME } from './constants'; +import Logger from '../../utils/logger'; +import { getDestinationOptions, sanitizeName, sanitizeAttributes } from './utils'; +import { loadNativeSdk } from './nativeSdkLoader'; + +const logger = new Logger(DISPLAY_NAME); + +class Wingify { + constructor(config, analytics, destinationInfo) { + if (analytics.logLevel) { + logger.setLogLevel(analytics.logLevel); + } + this.analytics = analytics; + this.accountId = config.accountId; + this.settingsTolerance = config.settingsTolerance; + this.isSPA = config.isSPA; + this.libraryTolerance = config.libraryTolerance; + this.useExistingJquery = config.useExistingJquery; + this.sendExperimentTrack = config.sendExperimentTrack; + this.sendExperimentIdentify = config.sendExperimentIdentify; + this.name = NAME; + ({ + shouldApplyDeviceModeTransformation: this.shouldApplyDeviceModeTransformation, + propagateEventsUntransformedOnError: this.propagateEventsUntransformedOnError, + destinationId: this.destinationId, + } = destinationInfo ?? {}); + } + + init() { + const wingifyIntgConfig = getDestinationOptions(this.analytics.loadOnlyIntegrations); + if (wingifyIntgConfig?.loadIntegration) { + const account_id = this.accountId; + const settings_tolerance = this.settingsTolerance; + loadNativeSdk(account_id, settings_tolerance); + } else { + logger.info('loadIntegration flag is disabled'); + } + + window.WINGIFY = window.WINGIFY || []; + window.WINGIFY.event = + window.WINGIFY.event || + function (...args) { + window.WINGIFY.push(['event', ...args]); + }; + + window.WINGIFY.visitor = + window.WINGIFY.visitor || + function (...args) { + window.WINGIFY.push(['visitor', ...args]); + }; + + if (this.sendExperimentTrack || this.sendExperimentIdentify) { + this.experimentViewed(); + } + } + + isLoaded() { + return !!window._wingify_code; + } + + isReady() { + return this.isLoaded(); + } + + experimentViewed() { + window.WINGIFY = window.WINGIFY || []; + window.WINGIFY.push([ + 'onVariationApplied', + data => { + if (!data) { + return; + } + const expId = data[1]; + const variationId = data[2]; + const experiment = window._wingify_exp?.[expId]; + const variationName = experiment?.comb_n?.[variationId]; + + if ( + typeof variationName === 'undefined' || + ['VISUAL_AB', 'VISUAL', 'SPLIT_URL', 'SURVEY'].indexOf(experiment?.type) === -1 + ) { + return; + } + + logger.info('experiment id:', expId, 'Variation Name:', variationName); + try { + if (this.sendExperimentTrack) { + this.analytics.track('Experiment Viewed', { + experimentId: expId, + variationName, + CampaignName: experiment.name, + VariationId: variationId, + }); + } + } catch (error) { + logger.error('experimentViewed', error); + } + try { + if (this.sendExperimentIdentify) { + this.analytics.identify({ + [`Experiment: ${expId}`]: variationName, + }); + } + } catch (error) { + logger.error('experimentViewed', error); + } + }, + ]); + } + + identify(rudderElement) { + const { message } = rudderElement; + const payload = message.context?.traits ?? message.traits ?? {}; + const formattedAttributes = sanitizeAttributes(payload); + + window.WINGIFY.visitor(formattedAttributes, { source: 'rudderstack' }); + } + + track(rudderElement) { + const eventName = rudderElement.message.event?.trim(); + if (!eventName) { + logger.error('[WINGIFY] track:: event name is required'); + return; + } + const properties = rudderElement.message?.properties || {}; + window.WINGIFY = window.WINGIFY || []; + if (eventName === 'Order Completed') { + const total = rudderElement.message.properties + ? rudderElement.message.properties.total || rudderElement.message.properties.revenue + : 0; + window.WINGIFY = window.WINGIFY || []; + window.WINGIFY.push(['track.revenueConversion', total]); + } + const sanitizedEventName = sanitizeName(eventName); + logger.debug(`[WINGIFY] eventName: ${sanitizedEventName}`); + window.WINGIFY.event(sanitizedEventName, properties, { source: 'rudderstack', ogName: eventName }); + } +} + +export default Wingify; diff --git a/packages/analytics-js-integrations/src/integrations/Wingify/constants.js b/packages/analytics-js-integrations/src/integrations/Wingify/constants.js new file mode 100644 index 000000000..ba123115d --- /dev/null +++ b/packages/analytics-js-integrations/src/integrations/Wingify/constants.js @@ -0,0 +1,11 @@ +import { WINGIFY_NAME as NAME, WINGIFY_DISPLAY_NAME as DISPLAY_NAME } from '../../constants/Destinations'; + +const DIR_NAME = 'Wingify'; + +const CNameMapping = { + [NAME]: NAME, + Wingify: NAME, + wingify: NAME, +}; + +export { NAME, CNameMapping, DISPLAY_NAME, DIR_NAME }; diff --git a/packages/analytics-js-integrations/src/integrations/Wingify/index.js b/packages/analytics-js-integrations/src/integrations/Wingify/index.js new file mode 100644 index 000000000..4b20b353c --- /dev/null +++ b/packages/analytics-js-integrations/src/integrations/Wingify/index.js @@ -0,0 +1 @@ +export { default as Wingify } from './browser'; diff --git a/packages/analytics-js-integrations/src/integrations/Wingify/nativeSdkLoader.js b/packages/analytics-js-integrations/src/integrations/Wingify/nativeSdkLoader.js new file mode 100644 index 000000000..184012296 --- /dev/null +++ b/packages/analytics-js-integrations/src/integrations/Wingify/nativeSdkLoader.js @@ -0,0 +1,99 @@ +import { LOAD_ORIGIN } from '@rudderstack/analytics-js-legacy-utilities/constants'; + +function loadNativeSdk(account_id, settings_tolerance) { + if (window._wingify_code) { + return; + } + + const version = 3.0; + const hide_element = 'body'; + const hide_element_style = + 'opacity:0 !important;filter:alpha(opacity=0) !important;background:none !important;transition:none !important'; + + const t = window; + const n = document; + + if (n.URL.indexOf('__wingify_disable__') !== -1) { + return; + } + + let finished = false; + const o = n.currentScript; + let config = { sT: settings_tolerance, hES: hide_element_style, hE: hide_element }; + + try { + const storedConfig = localStorage.getItem(`_wingify_${account_id}_config`); + if (storedConfig) { + config = Object.assign(JSON.parse(storedConfig), config); + } + } catch (e) { + // ignore invalid localStorage config + } + + const code = { + nonce: o?.nonce, + settings_tolerance() { + return config.sT; + }, + hide_element() { + if ( + typeof performance !== 'undefined' && + typeof performance.getEntriesByName === 'function' && + performance.getEntriesByName('first-contentful-paint')[0] + ) { + return ''; + } + return config.hE; + }, + hide_element_style() { + return `{${config.hES}}`; + }, + getVersion() { + return version; + }, + finish() { + if (!finished) { + finished = true; + const hideEl = n.getElementById('_vis_opt_path_hides'); + if (hideEl) { + hideEl.parentNode.removeChild(hideEl); + } + } + }, + finished() { + return finished; + }, + addScript(src) { + const script = n.createElement('script'); + script.src = src; + if (o?.nonce) { + script.setAttribute('nonce', o.nonce); + } + script.setAttribute('data-loader', LOAD_ORIGIN); + script.fetchPriority = 'high'; + n.head.appendChild(script); + }, + init() { + t._settings_timer = setTimeout(() => { + code.finish(); + }, this.settings_tolerance()); + + const hideElement = this.hide_element(); + if (hideElement) { + const style = n.createElement('style'); + style.id = '_vis_opt_path_hides'; + if (o?.nonce) { + style.setAttribute('nonce', o.nonce); + } + style.textContent = hideElement + this.hide_element_style(); + n.head.appendChild(style); + } + this.addScript(`https://edge.wingify.net/tag/${account_id}.js`); + }, + }; + + t._wingify_code = code; + code.init(); +} + +export { loadNativeSdk }; diff --git a/packages/analytics-js-integrations/src/integrations/Wingify/utils.js b/packages/analytics-js-integrations/src/integrations/Wingify/utils.js new file mode 100644 index 000000000..b4fcbedad --- /dev/null +++ b/packages/analytics-js-integrations/src/integrations/Wingify/utils.js @@ -0,0 +1,37 @@ +import { NAME, DISPLAY_NAME } from './constants'; + +/** + * Get destination specific options from integrations options + * By default, it will return options for the destination using its display name + * If display name is not present, it will return options for the destination using its name + * The fallback is only for backward compatibility with SDK versions < v1.1 + * @param {object} integrationsOptions Integrations options object + * @returns destination specific options + */ +const getDestinationOptions = integrationsOptions => + integrationsOptions && (integrationsOptions[DISPLAY_NAME] || integrationsOptions[NAME]); + +/** + * Sanitizes the given event name by trimming any leading or trailing whitespace and prefixing it with "rudder.". + * + * @param {string} eventName - The event name to be sanitized. + * @return {string} The sanitized event name. + */ +const sanitizeName = eventName => `rudder.${eventName.trim()}`; + +/** + * Sanitizes the attributes object by formatting the keys and returning a new object with the formatted keys. + * + * @param {object} attributes - The attributes object to be sanitized. + * @return {object} - The sanitized attributes object with formatted keys. + */ +const sanitizeAttributes = attributes => { + const formattedAttributes = {}; + for (const key of Object.keys(attributes)) { + const formattedKey = sanitizeName(key); + formattedAttributes[formattedKey] = attributes[key]; + } + return formattedAttributes; +}; + +export { getDestinationOptions, sanitizeName, sanitizeAttributes }; diff --git a/packages/analytics-js-integrations/src/integrations/index.js b/packages/analytics-js-integrations/src/integrations/index.js index 5bc1c3fa4..cce0b797b 100644 --- a/packages/analytics-js-integrations/src/integrations/index.js +++ b/packages/analytics-js-integrations/src/integrations/index.js @@ -39,6 +39,7 @@ import * as Sentry from './Sentry'; import * as SnapPixel from './SnapPixel'; import * as TVSquared from './TVSquared'; import * as VWO from './VWO'; +import * as Wingify from './Wingify'; import * as GoogleOptimize from './GoogleOptimize'; import * as PostAffiliatePro from './PostAffiliatePro'; import * as LaunchDarkly from './LaunchDarkly'; @@ -126,6 +127,7 @@ const integrations = { SNAP_PIXEL: SnapPixel.default, TVSQUARED: TVSquared.default, VWO: VWO.default, + WINGIFY: Wingify.Wingify, GOOGLE_OPTIMIZE: GoogleOptimize.default, POST_AFFILIATE_PRO: PostAffiliatePro.default, LAUNCHDARKLY: LaunchDarkly.default, diff --git a/packages/analytics-js-legacy-utilities/src/config_to_integration_names.js b/packages/analytics-js-legacy-utilities/src/config_to_integration_names.js index bc9db2000..a3443ef1b 100644 --- a/packages/analytics-js-legacy-utilities/src/config_to_integration_names.js +++ b/packages/analytics-js-legacy-utilities/src/config_to_integration_names.js @@ -6,6 +6,7 @@ const configToIntNames = { HOTJAR: 'Hotjar', GOOGLEADS: 'GoogleAds', VWO: 'VWO', + WINGIFY: 'Wingify', GTM: 'GoogleTagManager', BRAZE: 'Braze', INTERCOM: 'INTERCOM', diff --git a/packages/analytics-js-plugins/src/utilities/destDisplayNamesToFileNamesMap.ts b/packages/analytics-js-plugins/src/utilities/destDisplayNamesToFileNamesMap.ts index 5183a013e..4a73c2f16 100644 --- a/packages/analytics-js-plugins/src/utilities/destDisplayNamesToFileNamesMap.ts +++ b/packages/analytics-js-plugins/src/utilities/destDisplayNamesToFileNamesMap.ts @@ -21,6 +21,10 @@ import { DISPLAY_NAME as VWODisplayName, DIR_NAME as VWODirectoryName, } from '@rudderstack/analytics-js-integrations/integrations/VWO/constants'; +import { + DISPLAY_NAME as WingifyDisplayName, + DIR_NAME as WingifyDirectoryName, +} from '@rudderstack/analytics-js-integrations/integrations/Wingify/constants'; import { DISPLAY_NAME as GoogleTagManagerDisplayName, DIR_NAME as GoogleTagManagerDirectoryName, @@ -337,6 +341,7 @@ const destDisplayNamesToFileNamesMap: Record = { [HotjarDisplayName]: HotjarDirectoryName, [GoogleAdsDisplayName]: GoogleAdsDirectoryName, [VWODisplayName]: VWODirectoryName, + [WingifyDisplayName]: WingifyDirectoryName, [GoogleTagManagerDisplayName]: GoogleTagManagerDirectoryName, [BrazeDisplayName]: BrazeDirectoryName, [IntercomDisplayName]: IntercomDirectoryName, diff --git a/packages/analytics-v1.1/src/constants/integrations/client_server_name.js b/packages/analytics-v1.1/src/constants/integrations/client_server_name.js index 1ea06e5b6..f279e6422 100644 --- a/packages/analytics-v1.1/src/constants/integrations/client_server_name.js +++ b/packages/analytics-v1.1/src/constants/integrations/client_server_name.js @@ -18,6 +18,7 @@ const clientToServerNames = { KISSMETRICS: 'Kiss Metrics', LOTAME: 'Lotame', VWO: 'VWO', + WINGIFY: 'Wingify', OPTIMIZELY: 'Optimizely Web', FULLSTORY: 'Fullstory', TVSQUARED: 'TVSquared', diff --git a/packages/analytics-v1.1/src/constants/integrations/integration_cname.js b/packages/analytics-v1.1/src/constants/integrations/integration_cname.js index bca9b2d8e..101d604e2 100644 --- a/packages/analytics-v1.1/src/constants/integrations/integration_cname.js +++ b/packages/analytics-v1.1/src/constants/integrations/integration_cname.js @@ -45,6 +45,7 @@ import { CNameMapping as Sentry } from '@rudderstack/analytics-js-integrations/i import { CNameMapping as SnapPixel } from '@rudderstack/analytics-js-integrations/integrations/SnapPixel/constants'; import { CNameMapping as TVSquared } from '@rudderstack/analytics-js-integrations/integrations/TVSquared/constants'; import { CNameMapping as VWO } from '@rudderstack/analytics-js-integrations/integrations/VWO/constants'; +import { CNameMapping as Wingify } from '@rudderstack/analytics-js-integrations/integrations/Wingify/constants'; import { CNameMapping as GA360 } from '@rudderstack/analytics-js-integrations/integrations/GA360/constants'; import { CNameMapping as Adroll } from '@rudderstack/analytics-js-integrations/integrations/Adroll/constants'; import { CNameMapping as DCMFloodlight } from '@rudderstack/analytics-js-integrations/integrations/DCMFloodlight/constants'; @@ -134,6 +135,7 @@ const commonNames = { ...SnapPixel, ...TVSquared, ...VWO, + ...Wingify, ...Adroll, ...DCMFloodlight, ...Matomo,