From 4ac902ecf81e04b6cc36e41094bb3da3f18bf09c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 4 Oct 2024 17:15:28 -0300 Subject: [PATCH] Removed the deprecated and integrations --- CHANGES.txt | 3 +- src/integrations/__tests__/browser.spec.ts | 98 ------ src/integrations/browser.ts | 35 -- src/integrations/ga/GaToSplit.ts | 299 ------------------ src/integrations/ga/GoogleAnalyticsToSplit.ts | 14 - src/integrations/ga/SplitToGa.ts | 135 -------- src/integrations/ga/SplitToGoogleAnalytics.ts | 14 - .../ga/__tests__/GaToSplit.spec.ts | 295 ----------------- .../ga/__tests__/SplitToGa.spec.ts | 195 ------------ src/integrations/ga/__tests__/gaMock.ts | 60 ---- src/integrations/ga/autoRequire.js | 33 -- src/integrations/ga/types.ts | 153 --------- src/utils/constants/browser.ts | 4 - .../integrations/__tests__/plugable.spec.ts | 4 +- 14 files changed, 4 insertions(+), 1338 deletions(-) delete mode 100644 src/integrations/__tests__/browser.spec.ts delete mode 100644 src/integrations/browser.ts delete mode 100644 src/integrations/ga/GaToSplit.ts delete mode 100644 src/integrations/ga/GoogleAnalyticsToSplit.ts delete mode 100644 src/integrations/ga/SplitToGa.ts delete mode 100644 src/integrations/ga/SplitToGoogleAnalytics.ts delete mode 100644 src/integrations/ga/__tests__/GaToSplit.spec.ts delete mode 100644 src/integrations/ga/__tests__/SplitToGa.spec.ts delete mode 100644 src/integrations/ga/__tests__/gaMock.ts delete mode 100644 src/integrations/ga/autoRequire.js delete mode 100644 src/integrations/ga/types.ts diff --git a/CHANGES.txt b/CHANGES.txt index 8b3e1c91..9ec1a0e7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,8 +1,9 @@ -2.0.0 (September XX, 2024) +2.0.0 (October XX, 2024) - Added support for targeting rules based on large segments. - Added `factory.destroy()` method, which invokes the `destroy` method on all SDK clients created by the factory. - Bugfixing - Fixed an issue with the server-side polling manager that caused dangling timers when the SDK was destroyed before it was ready. - BREAKING CHANGES: + - Removed the deprecated `GOOGLE_ANALYTICS_TO_SPLIT` and `SPLIT_TO_GOOGLE_ANALYTICS` integrations. - Updated default flag spec version to 1.2. - Removed `/mySegments` endpoint from SplitAPI module, as it is replaced by `/memberships` endpoint. - Removed support for MY_SEGMENTS_UPDATE and MY_SEGMENTS_UPDATE_V2 notification types, as they are replaced by MEMBERSHIPS_MS_UPDATE and MEMBERSHIPS_LS_UPDATE notification types. diff --git a/src/integrations/__tests__/browser.spec.ts b/src/integrations/__tests__/browser.spec.ts deleted file mode 100644 index 40316654..00000000 --- a/src/integrations/__tests__/browser.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { GOOGLE_ANALYTICS_TO_SPLIT, SPLIT_TO_GOOGLE_ANALYTICS } from '../../utils/constants/browser'; -import { SPLIT_IMPRESSION, SPLIT_EVENT } from '../../utils/constants'; -import { IIntegrationManager } from '../types'; -import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; - -// Mock integration modules (GaToSplit and SplitToGa). - -jest.mock('../ga/GaToSplit'); -import { GaToSplit as GaToSplitMock } from '../ga/GaToSplit'; -jest.mock('../ga/SplitToGa'); -import { SplitToGa as SplitToGaMock } from '../ga/SplitToGa'; - -const SplitToGaQueueMethod = jest.fn(); -(SplitToGaMock as unknown as jest.Mock).mockImplementation(() => { - return { - queue: SplitToGaQueueMethod - }; -}); - - -const fakeParams = { - storage: 'fakeStorage', - settings: { - core: 'fakeCore', - log: loggerMock - } -}; - -function clearMocks() { - (GaToSplitMock as jest.Mock).mockClear(); - (SplitToGaMock as unknown as jest.Mock).mockClear(); - SplitToGaQueueMethod.mockClear(); -} - -// Test target -import { integrationsManagerFactory as browserIMF } from '../browser'; -import { BrowserIntegration } from '../ga/types'; - -describe('IntegrationsManagerFactory for browser', () => { - - test('API', () => { - expect(typeof browserIMF).toBe('function'); // The module should return a function which acts as a factory. - - // @ts-expect-error - const instance1 = browserIMF([]); - expect(instance1).toBe(undefined); // The instance should be undefined if settings.integrations does not contain integrations that register a listener. - - let integrations: BrowserIntegration[] = [{ type: GOOGLE_ANALYTICS_TO_SPLIT }, { type: SPLIT_TO_GOOGLE_ANALYTICS }]; - const instance2 = browserIMF(integrations, fakeParams as any) as IIntegrationManager; - expect(GaToSplitMock).toBeCalledTimes(1); // GaToSplit invoked once - expect(SplitToGaMock).toBeCalledTimes(1); // SplitToGa invoked once - expect(typeof instance2.handleImpression).toBe('function'); // The instance should implement the handleImpression method if settings.integrations has items that register a listener. - expect(typeof instance2.handleEvent).toBe('function'); // The instance should implement the handleEvent method if settings.integrations has items that register a listener. - - clearMocks(); - - integrations = [{ type: GOOGLE_ANALYTICS_TO_SPLIT }, { type: SPLIT_TO_GOOGLE_ANALYTICS }, { type: GOOGLE_ANALYTICS_TO_SPLIT }, { type: SPLIT_TO_GOOGLE_ANALYTICS }, { type: SPLIT_TO_GOOGLE_ANALYTICS }]; - browserIMF(integrations, fakeParams as any); - expect(GaToSplitMock).toBeCalledTimes(2); // GaToSplit invoked twice - expect(SplitToGaMock).toBeCalledTimes(3); // SplitToGa invoked thrice - - clearMocks(); - }); - - test('Interaction with GaToSplit integration module', () => { - const integrations: BrowserIntegration[] = [{ - type: 'GOOGLE_ANALYTICS_TO_SPLIT', - prefix: 'some-prefix' - }]; - browserIMF(integrations, fakeParams as any); - - expect((GaToSplitMock as jest.Mock).mock.calls).toEqual([[integrations[0], fakeParams]]); // Invokes GaToSplit integration module with options, storage and core settings - - clearMocks(); - }); - - test('Interaction with SplitToGa integration module', () => { - const integrations: BrowserIntegration[] = [{ - type: 'SPLIT_TO_GOOGLE_ANALYTICS', - events: true - }]; - const instance = browserIMF(integrations, fakeParams as any); - - expect((SplitToGaMock as unknown as jest.Mock).mock.calls).toEqual([[fakeParams.settings.log, integrations[0]]]); // Invokes SplitToGa integration module with options - - const fakeImpression = 'fake'; // @ts-expect-error - instance.handleImpression(fakeImpression); - expect(SplitToGaQueueMethod.mock.calls).toEqual([[{ payload: fakeImpression, type: SPLIT_IMPRESSION }]]); // Invokes SplitToGa.queue method with tracked impression - - clearMocks(); - - const fakeEvent = 'fake'; // @ts-expect-error - instance.handleEvent(fakeEvent); - expect(SplitToGaQueueMethod.mock.calls).toEqual([[{ payload: fakeEvent, type: SPLIT_EVENT }]]); // Invokes SplitToGa.queue method with tracked event - - clearMocks(); - }); -}); diff --git a/src/integrations/browser.ts b/src/integrations/browser.ts deleted file mode 100644 index d4ad8de8..00000000 --- a/src/integrations/browser.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { GOOGLE_ANALYTICS_TO_SPLIT, SPLIT_TO_GOOGLE_ANALYTICS } from '../utils/constants/browser'; -import { IIntegration, IIntegrationManager, IIntegrationFactoryParams } from './types'; -import { BrowserIntegration } from './ga/types'; -import { pluggableIntegrationsManagerFactory } from './pluggable'; -import { GoogleAnalyticsToSplit } from './ga/GoogleAnalyticsToSplit'; -import { SplitToGoogleAnalytics } from './ga/SplitToGoogleAnalytics'; - -/** - * IntegrationsManager factory for the browser variant of the isomorphic JS SDK. - * The integrations manager instantiates integration modules, and bypass tracked events and impressions to them. - * - * @param integrations valid integration settings object for browser sdk - * @param params information of the Sdk factory instance that integrations can access to - * - * @returns integration manager or undefined if `integrations` are not present in settings. - */ -export function integrationsManagerFactory( - integrations: BrowserIntegration[], - params: IIntegrationFactoryParams -): IIntegrationManager | undefined { - - // maps integration config items into integration factories to reuse the pluggable integration manager - const integrationFactories: Array<(params: IIntegrationFactoryParams) => IIntegration | void> = integrations - .map(integrationOptions => { - switch (integrationOptions.type) { - case GOOGLE_ANALYTICS_TO_SPLIT: return GoogleAnalyticsToSplit(integrationOptions); - case SPLIT_TO_GOOGLE_ANALYTICS: return SplitToGoogleAnalytics(integrationOptions); - } - }) - .filter(integrationFactory => { - return integrationFactory && typeof integrationFactory === 'function'; - }); - - return pluggableIntegrationsManagerFactory(integrationFactories, params); -} diff --git a/src/integrations/ga/GaToSplit.ts b/src/integrations/ga/GaToSplit.ts deleted file mode 100644 index a996625f..00000000 --- a/src/integrations/ga/GaToSplit.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { objectAssign } from '../../utils/lang/objectAssign'; -import { isString, isFiniteNumber, uniqAsStrings } from '../../utils/lang'; -import { - validateEvent, - validateEventValue, - validateEventProperties, - validateKey, - validateTrafficType -} from '../../utils/inputValidation'; -import { SplitIO } from '../../types'; -import { Identity, GoogleAnalyticsToSplitOptions } from './types'; -import { ILogger } from '../../logger/types'; -import { IIntegrationFactoryParams } from '../types'; -import { ITelemetryTracker } from '../../trackers/types'; - -const logPrefix = 'ga-to-split: '; -const logNameMapper = 'ga-to-split:mapper'; - -/** - * Provides a plugin to use with analytics.js, accounting for the possibility - * that the global command queue has been renamed or not yet defined. - * @param window Reference to global object. - * @param pluginName The plugin name identifier. - * @param pluginConstructor The plugin constructor function. - * @param log Logger instance. - * @param autoRequire If true, log error when auto-require script is not detected - */ -function providePlugin(window: any, pluginName: string, pluginConstructor: Function, log: ILogger, autoRequire: boolean, telemetryTracker?: ITelemetryTracker) { - // get reference to global command queue. Init it if not defined yet. - const gaAlias = window.GoogleAnalyticsObject || 'ga'; - window[gaAlias] = window[gaAlias] || function () { - (window[gaAlias].q = window[gaAlias].q || []).push(arguments); - }; - - // provides the plugin for use with analytics.js. - window[gaAlias]('provide', pluginName, pluginConstructor); - - const hasAutoRequire = window[gaAlias].q && window[gaAlias].q.push !== [].push; - if (autoRequire && !hasAutoRequire) { // Expecting spy on ga.q push method but not found - log.error(logPrefix + 'integration is configured to autorequire the splitTracker plugin, but the necessary script does not seem to have run. Please check the docs.'); - } - if (telemetryTracker && hasAutoRequire) { - telemetryTracker.addTag('integration:ga-autorequire'); - } -} - -// Default mapping: object used for building the default mapper from hits to Split events -const defaultMapping = { - eventTypeId: { - event: 'eventAction', - social: 'socialAction', - }, - eventValue: { - event: 'eventValue', - timing: 'timingValue', - }, - eventProperties: { - pageview: ['page'], - screenview: ['screenName'], - event: ['eventCategory', 'eventLabel'], - social: ['socialNetwork', 'socialTarget'], - timing: ['timingCategory', 'timingVar', 'timingLabel'], - exception: ['exDescription', 'exFatal'], - } -}; - -/** - * Build a mapper function based on a mapping object - * - * @param {object} mapping - */ -function mapperBuilder(mapping: typeof defaultMapping) { - return function (model: UniversalAnalytics.Model): SplitIO.EventData { - const hitType: string = model.get('hitType'); - // @ts-expect-error - const eventTypeId = model.get(mapping.eventTypeId[hitType] || 'hitType'); - // @ts-expect-error - const value = model.get(mapping.eventValue[hitType]); - - const properties: Record = {}; // @ts-expect-error - const fields: string[] = mapping.eventProperties[hitType]; - if (fields) { - for (let i = 0; i < fields.length; i++) { - const fieldValue = model.get(fields[i]); - if (fieldValue !== undefined) properties[fields[i]] = fieldValue; - } - } - - return { - eventTypeId, - value, - properties, - timestamp: Date.now(), - }; - }; -} - -// exposed for unit testing purposses. -export const defaultMapper = mapperBuilder(defaultMapping); - -export const defaultPrefix = 'ga'; - -/** - * Return a new list of identities removing invalid and duplicated ones. - * - * @param {Array} identities list of identities - * @returns list of valid and unique identities. The list might be empty if `identities` is not an array or all its elements are invalid. - */ -export function validateIdentities(identities?: Identity[]) { - if (!Array.isArray(identities)) - return []; - - // Remove duplicated identities - const uniqueIdentities = uniqAsStrings(identities); - - // Filter based on rum-agent identities validator - return uniqueIdentities.filter(identity => { - if (!identity) return false; - - const maybeKey = identity.key; - const maybeTT = identity.trafficType; - - if (!isString(maybeKey) && !isFiniteNumber(maybeKey)) - return false; - if (!isString(maybeTT)) - return false; - - return true; - }); -} - -/** - * Checks if EventData fields (except EventTypeId) are valid, and logs corresponding warnings. - * EventTypeId is validated separately. - * - * @param {EventData} data event data instance to validate. Precondition: data != undefined - * @returns {boolean} Whether the data instance is a valid EventData or not. - */ -export function validateEventData(log: ILogger, eventData: any): eventData is SplitIO.EventData { - if (!validateEvent(log, eventData.eventTypeId, logNameMapper)) - return false; - - if (validateEventValue(log, eventData.value, logNameMapper) === false) - return false; - - const { properties } = validateEventProperties(log, eventData.properties, logNameMapper); - if (properties === false) - return false; - - if (eventData.timestamp && !isFiniteNumber(eventData.timestamp)) - return false; - - if (eventData.key && validateKey(log, eventData.key, logNameMapper) === false) - return false; - - if (eventData.trafficTypeName && validateTrafficType(log, eventData.trafficTypeName, logNameMapper) === false) - return false; - - return true; -} - -const INVALID_PREFIX_REGEX = /^[^a-zA-Z0-9]+/; -const INVALID_SUBSTRING_REGEX = /[^-_.:a-zA-Z0-9]+/g; -/** - * Fixes the passed string value to comply with EventTypeId format, by removing invalid characters and truncating if necessary. - * - * @param {object} log factory logger - * @param {string} eventTypeId string value to fix. - * @returns {string} Fixed version of `eventTypeId`. - */ -export function fixEventTypeId(log: ILogger, eventTypeId: any) { - // return the input eventTypeId if it cannot be fixed - if (!isString(eventTypeId) || eventTypeId.length === 0) { - return eventTypeId; - } - - // replace invalid substrings and truncate - const fixed = eventTypeId - .replace(INVALID_PREFIX_REGEX, '') - .replace(INVALID_SUBSTRING_REGEX, '_'); - const truncated = fixed.slice(0, 80); - if (truncated.length < fixed.length) log.warn(logPrefix + 'EventTypeId was truncated because it cannot be more than 80 characters long.'); - return truncated; -} - -/** - * GaToSplit integration. - * This function provides the SplitTracker plugin to ga command queue. - * - * @param {object} sdkOptions options passed at the SDK integrations settings (isomorphic SDK) or the GoogleAnalyticsToSplit plugin (pluggable browser SDK) - * @param {object} storage SDK storage passed to track events - * @param {object} coreSettings core settings used to define an identity if no one provided as SDK or plugin options - * @param {object} log factory logger - */ -export function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, params: IIntegrationFactoryParams) { - - const { storage, settings: { core: coreSettings, log }, telemetryTracker } = params; - - const defaultOptions = { - prefix: defaultPrefix, - // We set default identities if key and TT are present in settings.core - identities: (coreSettings.key && coreSettings.trafficType) ? - [{ key: coreSettings.key, trafficType: coreSettings.trafficType }] : - undefined - }; - - class SplitTracker { - - private tracker: UniversalAnalytics.Tracker; - - // Constructor for the SplitTracker plugin. - constructor(tracker: UniversalAnalytics.Tracker, pluginOptions: GoogleAnalyticsToSplitOptions) { - - // precedence of options: SDK options (config.integrations) overwrite pluginOptions (`ga('require', 'splitTracker', pluginOptions)`) - const opts = objectAssign({}, defaultOptions, sdkOptions, pluginOptions) as GoogleAnalyticsToSplitOptions & { identities: Identity[] }; - - this.tracker = tracker; - - // Validate identities - const validIdentities = validateIdentities(opts.identities); - - if (validIdentities.length === 0) { - log.warn(logPrefix + 'No valid identities were provided. Please check that you are passing a valid list of identities or providing a traffic type at the SDK configuration.'); - return; - } - - const invalids = validIdentities.length - opts.identities.length; - if (invalids) { - log.warn(logPrefix + `${invalids} identities were discarded because they are invalid or duplicated. Identities must be an array of objects with key and trafficType.`); - } - opts.identities = validIdentities; - - // Validate prefix - if (!isString(opts.prefix)) { - log.warn(logPrefix + 'The provided `prefix` was ignored since it is invalid. Please check that you are passing a string object as `prefix`.'); - opts.prefix = undefined; - } - - // Overwrite sendHitTask to perform plugin tasks: - // 1) filter hits - // 2) map hits to Split events - // 3) handle events, i.e., validate and send them to Split BE - const originalSendHitTask = tracker.get('sendHitTask'); - tracker.set('sendHitTask', function (model: UniversalAnalytics.Model) { - originalSendHitTask(model); - - // filter hit if `hits` flag is false or if it comes from Split-to-GA integration - if (opts.hits === false || model.get('splitHit')) return; - try { - if (opts.filter && !opts.filter(model)) return; - } catch (err) { - log.warn(logPrefix + `custom filter threw: ${err}`); - return; - } - - // map hit into an EventData instance - let eventData: SplitIO.EventData = defaultMapper(model); - if (opts.mapper) { - try { - eventData = opts.mapper(model, eventData as SplitIO.EventData); - } catch (err) { - log.warn(logPrefix + `custom mapper threw: ${err}`); - return; - } - if (!eventData) - return; - } - - // Add prefix. Nothing is appended if the prefix is falsy, e.g. undefined or ''. - if (opts.prefix) eventData.eventTypeId = `${opts.prefix}.${eventData.eventTypeId}`; - - eventData.eventTypeId = fixEventTypeId(log, eventData.eventTypeId); - - if (!validateEventData(log, eventData)) - return; - - // Store the event - if (eventData.key && eventData.trafficTypeName) { - storage.events.track(eventData); - } else { // Store the event for each Key-TT pair (identities), if key and TT is not present in eventData - opts.identities.forEach(identity => { - const event = objectAssign({ - key: identity.key, - trafficTypeName: identity.trafficType, - }, eventData); - storage.events.track(event); - }); - } - }); - - log.info(logPrefix + 'integration started'); - } - - } - - // Register the plugin, even if config is invalid, since, if not provided, it will block `ga` command queue. - // eslint-disable-next-line no-undef - providePlugin(window, 'splitTracker', SplitTracker, log, sdkOptions.autoRequire === true, telemetryTracker); -} diff --git a/src/integrations/ga/GoogleAnalyticsToSplit.ts b/src/integrations/ga/GoogleAnalyticsToSplit.ts deleted file mode 100644 index b6463bb2..00000000 --- a/src/integrations/ga/GoogleAnalyticsToSplit.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IIntegrationFactoryParams, IntegrationFactory } from '../types'; -import { GaToSplit } from './GaToSplit'; -import { GoogleAnalyticsToSplitOptions } from './types'; - -export function GoogleAnalyticsToSplit(options: GoogleAnalyticsToSplitOptions = {}): IntegrationFactory { - - // GaToSplit integration factory - function GoogleAnalyticsToSplitFactory(params: IIntegrationFactoryParams) { - return GaToSplit(options, params); - } - - GoogleAnalyticsToSplitFactory.type = 'GOOGLE_ANALYTICS_TO_SPLIT'; - return GoogleAnalyticsToSplitFactory; -} diff --git a/src/integrations/ga/SplitToGa.ts b/src/integrations/ga/SplitToGa.ts deleted file mode 100644 index dd469676..00000000 --- a/src/integrations/ga/SplitToGa.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* eslint-disable no-undef */ -import { uniq } from '../../utils/lang'; -import { SPLIT_IMPRESSION, SPLIT_EVENT } from '../../utils/constants'; -import { SplitIO } from '../../types'; -import { IIntegration } from '../types'; -import { SplitToGoogleAnalyticsOptions } from './types'; -import { ILogger } from '../../logger/types'; - -const logPrefix = 'split-to-ga: '; -const noGaWarning = '`ga` command queue not found.'; -const noHit = 'No hit was sent.'; - -export class SplitToGa implements IIntegration { - - // A falsy object represents the default tracker - static defaultTrackerNames = ['']; - - private trackerNames: string[]; - private filter?: (data: SplitIO.IntegrationData) => boolean; - private mapper?: (data: SplitIO.IntegrationData, defaultMapping: UniversalAnalytics.FieldsObject) => UniversalAnalytics.FieldsObject; - private impressions: boolean | undefined; - private events: boolean | undefined; - private log: ILogger; - - // Default mapper function. - static defaultMapper({ type, payload }: SplitIO.IntegrationData): UniversalAnalytics.FieldsObject { - switch (type) { - case SPLIT_IMPRESSION: - return { - hitType: 'event', - eventCategory: 'split-impression', - eventAction: 'Evaluate ' + (payload as SplitIO.ImpressionData).impression.feature, - eventLabel: 'Treatment: ' + (payload as SplitIO.ImpressionData).impression.treatment + '. Targeting rule: ' + (payload as SplitIO.ImpressionData).impression.label + '.', - nonInteraction: true, - }; - case SPLIT_EVENT: - return { - hitType: 'event', - eventCategory: 'split-event', - eventAction: (payload as SplitIO.EventData).eventTypeId, - eventValue: (payload as SplitIO.EventData).value, - nonInteraction: true, - }; - } - } - - // Util to access ga command queue, accounting for the possibility that it has been renamed. - static getGa(): UniversalAnalytics.ga | undefined { // @ts-expect-error - return typeof window !== 'undefined' ? window[window['GoogleAnalyticsObject'] || 'ga'] : undefined; - } - - /** - * Validates if a given object is a UniversalAnalytics.FieldsObject instance, and logs a warning if not. - * It checks that the object contains a `hitType`, since it is the minimal field required to send the hit - * and avoid the GA error `No hit type specified. Aborting hit.`. - * Other validations (e.g., an `event` hitType must have a `eventCategory` and `eventAction`) are handled - * and logged (as warnings or errors depending the case) by GA debugger, but the hit is sent anyway. - * - * @param {object} log factory logger - * @param {UniversalAnalytics.FieldsObject} fieldsObject object to validate. - * @returns {boolean} Whether the data instance is a valid FieldsObject or not. - */ - static validateFieldsObject(log: ILogger, fieldsObject: any): fieldsObject is UniversalAnalytics.FieldsObject { - if (fieldsObject && fieldsObject.hitType) return true; - - log.warn(logPrefix + 'your custom mapper returned an invalid FieldsObject instance. It must be an object with at least a `hitType` field.'); - return false; - } - - /** - * constructor description - * @param {object} options options passed at the SDK integrations settings (isomorphic SDK) or the SplitToGoogleAnalytics plugin (pluggable browser SDK) - */ - constructor(log: ILogger, options: SplitToGoogleAnalyticsOptions) { - - this.trackerNames = SplitToGa.defaultTrackerNames; - this.log = log; - - if (options) { - if (typeof options.filter === 'function') this.filter = options.filter; - if (typeof options.mapper === 'function') this.mapper = options.mapper; - // We strip off duplicated values if we received a `trackerNames` param. - // We don't warn if a tracker does not exist, since the user might create it after the SDK is initialized. - // Note: GA allows to create and get trackers using a string or number as tracker name, and does nothing if other types are used. - if (Array.isArray(options.trackerNames)) this.trackerNames = uniq(options.trackerNames); - - // No need to validate `impressions` and `events` flags. Any other value than `false` is ignored (considered true by default). - this.impressions = options.impressions; - this.events = options.events; - } - - log.info(logPrefix + 'integration started'); - if (typeof SplitToGa.getGa() !== 'function') log.warn(logPrefix + `${noGaWarning} No hits will be sent until it is available.`); - } - - queue(data: SplitIO.IntegrationData) { - // access ga command queue via `getGa` method, accounting for the possibility that - // the global `ga` reference was not yet mutated by analytics.js. - const ga = SplitToGa.getGa(); - if (ga) { - - if (this.impressions === false && data.type === SPLIT_IMPRESSION) return; - if (this.events === false && data.type === SPLIT_EVENT) return; - - let fieldsObject: UniversalAnalytics.FieldsObject & { splitHit?: boolean }; - try { // only try/catch filter and mapper, which might be defined by the user - // filter - if (this.filter && !this.filter(data)) return; - - // map data into a FieldsObject instance - fieldsObject = SplitToGa.defaultMapper(data); - if (this.mapper) { - fieldsObject = this.mapper(data, fieldsObject); - // don't send the hit if it is falsy or invalid - if (!fieldsObject || !SplitToGa.validateFieldsObject(this.log, fieldsObject)) return; - } - } catch (err) { - this.log.warn(logPrefix + `queue method threw: ${err}. ${noHit}`); - return; - } - - // send the hit - this.trackerNames.forEach(trackerName => { - const sendCommand = trackerName ? `${trackerName}.send` : 'send'; - // mark the hit as a Split one to avoid the loop. - fieldsObject.splitHit = true; - // Send to GA using our reference to the GA object. - ga(sendCommand, fieldsObject); - }); - } else { - this.log.warn(logPrefix + `${noGaWarning} ${noHit}`); - } - } - -} diff --git a/src/integrations/ga/SplitToGoogleAnalytics.ts b/src/integrations/ga/SplitToGoogleAnalytics.ts deleted file mode 100644 index 101df26f..00000000 --- a/src/integrations/ga/SplitToGoogleAnalytics.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IIntegrationFactoryParams, IntegrationFactory } from '../types'; -import { SplitToGa } from './SplitToGa'; -import { SplitToGoogleAnalyticsOptions } from './types'; - -export function SplitToGoogleAnalytics(options: SplitToGoogleAnalyticsOptions = {}): IntegrationFactory { - - // SplitToGa integration factory - function SplitToGoogleAnalyticsFactory(params: IIntegrationFactoryParams) { - return new SplitToGa(params.settings.log, options); - } - - SplitToGoogleAnalyticsFactory.type = 'SPLIT_TO_GOOGLE_ANALYTICS'; - return SplitToGoogleAnalyticsFactory; -} diff --git a/src/integrations/ga/__tests__/GaToSplit.spec.ts b/src/integrations/ga/__tests__/GaToSplit.spec.ts deleted file mode 100644 index 1417c6a1..00000000 --- a/src/integrations/ga/__tests__/GaToSplit.spec.ts +++ /dev/null @@ -1,295 +0,0 @@ -/* eslint-disable no-undef */ -import { IEventsCacheSync } from '../../../storages/types'; -import { SplitIO, ISettings } from '../../../types'; -import { GaToSplit, validateIdentities, defaultPrefix, defaultMapper, validateEventData, fixEventTypeId } from '../GaToSplit'; -import { gaMock, gaRemove, modelMock } from './gaMock'; -import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; - -const hitSample: UniversalAnalytics.FieldsObject = { - hitType: 'pageview', - page: '/path', -}; -const modelSample = modelMock(hitSample); -const expectedDefaultEvent = { - eventTypeId: 'pageview', - value: undefined, - properties: { page: hitSample.page }, - timestamp: 0, -}; - -test('validateIdentities', () => { - expect(validateIdentities(undefined)).toEqual([]); // @ts-expect-error - expect(validateIdentities(null)).toEqual([]); // @ts-expect-error - expect(validateIdentities(123)).toEqual([]); // @ts-expect-error - expect(validateIdentities(true)).toEqual([]); // @ts-expect-error - expect(validateIdentities('something')).toEqual([]); // @ts-expect-error - expect(validateIdentities({})).toEqual([]); // @ts-expect-error - expect(validateIdentities(/asd/ig)).toEqual([]); // @ts-expect-error - expect(validateIdentities(function () { })).toEqual([]); - - expect(validateIdentities([])).toEqual([]); // @ts-expect-error - expect(validateIdentities([undefined, /asd/ig, function () { }])).toEqual([]); - expect(validateIdentities([{ - key: 'key', trafficType: 'user' // First occurence of this item - }, { // @ts-expect-error - key: 'key', trafficType: function () { } // Invalid item (invalid TT) - }, { - key: 'keyu', trafficType: 'ser' // First occurence of this item - }, { // @ts-expect-error - key: true, trafficType: 'user' // Invalid item (invalid key) - }, { - key: 'key2', trafficType: 'user2' // First occurence of this item - }, { // @ts-expect-error - key: 12, trafficType: 'user' // First occurence of this item - }, { - key: 'key', trafficType: 'user' // Duplicated item - }, // @ts-expect-error - {} // Invalid item (undefined key and traffic type) - ])).toEqual([{ - key: 'key', trafficType: 'user' - }, { - key: 'keyu', trafficType: 'ser' - }, { - key: 'key2', trafficType: 'user2' - }, { - key: 12, trafficType: 'user' - }]); -}); - -test('validateEventData', () => { - expect(() => { validateEventData(loggerMock, undefined); }).toThrow(); // throws exception if passed object is undefined - expect(() => { validateEventData(loggerMock, null); }).toThrow(); // throws exception if passed object is null - - expect(validateEventData(loggerMock, {})).toBe(false); // event must have a valid eventTypeId - expect(validateEventData(loggerMock, { eventTypeId: 'type' })).toBe(true); // event must have a valid eventTypeId - expect(validateEventData(loggerMock, { eventTypeId: 123 })).toBe(false); // event must have a valid eventTypeId - - expect(validateEventData(loggerMock, { eventTypeId: 'type', value: 'value' })).toBe(false); // event must have a valid value if present - expect(validateEventData(loggerMock, { eventTypeId: 'type', value: 0 })).toBe(true); // event must have a valid value if present - - expect(validateEventData(loggerMock, { eventTypeId: 'type', properties: ['prop1'] })).toBe(false); // event must have valid properties if present - expect(validateEventData(loggerMock, { eventTypeId: 'type', properties: { prop1: 'prop1' } })).toBe(true); // event must have valid properties if present - - expect(validateEventData(loggerMock, { eventTypeId: 'type', timestamp: true })).toBe(false); // event must have a valid timestamp if present - expect(validateEventData(loggerMock, { eventTypeId: 'type', timestamp: Date.now() })).toBe(true); // event must have a valid timestamp if present - - expect(validateEventData(loggerMock, { eventTypeId: 'type', key: true })).toBe(false); // event must have a valid key if present - expect(validateEventData(loggerMock, { eventTypeId: 'type', key: 'key' })).toBe(true); // event must have a valid key if present - - expect(validateEventData(loggerMock, { eventTypeId: 'type', trafficTypeName: true })).toBe(false); // event must have a valid trafficTypeName if present - expect(validateEventData(loggerMock, { eventTypeId: 'type', trafficTypeName: 'tt' })).toBe(true); // event must have a valid trafficTypeName if present -}); - -test('fixEventTypeId', () => { - expect(fixEventTypeId(loggerMock, undefined)).toBe(undefined); - expect(fixEventTypeId(loggerMock, 111)).toBe(111); - expect(fixEventTypeId(loggerMock, '')).toBe(''); - expect(fixEventTypeId(loggerMock, '()')).toBe(''); - expect(fixEventTypeId(loggerMock, '()+_')).toBe(''); - expect(fixEventTypeId(loggerMock, ' some event ')).toBe('some_event_'); - expect(fixEventTypeId(loggerMock, ' -*- some -.%^ event =+ ')).toBe('some_-._event_'); -}); - -test('defaultMapper', () => { - const initTimestamp = Date.now(); - const defaultEvent = defaultMapper(modelSample); - - expect(defaultEvent.eventTypeId).toBe(expectedDefaultEvent.eventTypeId); // should return the corresponding default event instance for a given pageview hit - expect(defaultEvent.value).toBe(expectedDefaultEvent.value); - expect(defaultEvent.properties).toEqual(expectedDefaultEvent.properties); - expect(initTimestamp <= defaultEvent.timestamp && defaultEvent.timestamp <= Date.now()).toBe(true); -}); - -const coreSettings = { - authorizationKey: 'sdkkey', - key: 'key', - trafficType: 'user', -} as ISettings['core']; -const fakeStorage = { - // @ts-expect-error - events: { - track: jest.fn() - } as IEventsCacheSync -}; -const fakeParams = { - storage: fakeStorage, - settings: { core: coreSettings, log: loggerMock } -}; - -// Returns a new event by copying defaultEvent -function customMapper(model: UniversalAnalytics.Model, defaultEvent: SplitIO.EventData) { - return { ...defaultEvent, properties: { ...defaultEvent.properties, someProp: 'someProp' } }; -} -// Updates defaultEvent -function customMapper2(model: UniversalAnalytics.Model, defaultEvent: SplitIO.EventData) { - // @ts-ignore. The defaultEvent has a property value, that might be empty depending on the hitType - defaultEvent.properties['someProp2'] = 'someProp2'; - return defaultEvent; -} -// Updates defaultEvent adding a `key` and `TT`, to assert that `identities` plugin param is ignored. -function customMapper3(model: UniversalAnalytics.Model, defaultEvent: SplitIO.EventData) { - defaultEvent.key = 'someKey'; - defaultEvent.trafficTypeName = 'someTT'; - return defaultEvent; -} -function customFilter() { - return true; -} -const customIdentities = [{ key: 'key2', trafficType: 'tt2' }]; - -test('GaToSplit', () => { - - // test setup - const { ga, tracker } = gaMock(); - - // provide SplitTracker plugin - GaToSplit({}, fakeParams as any); - // @ts-expect-error - let [arg1, arg2, SplitTracker] = ga.mock.calls.pop() as [string, string, any]; - expect([arg1, arg2]).toEqual(['provide', 'splitTracker']); - expect(typeof SplitTracker === 'function').toBe(true); - - /** Default behavior */ - - // init plugin on default tracker. equivalent to calling `ga('require', 'splitTracker')` - new SplitTracker(tracker); - - // send hit and assert that it was properly tracked as a Split event - window.ga('send', hitSample); - let event = (fakeStorage.events.track as jest.Mock).mock.calls.pop()[0]; - expect(event).toEqual( - { - ...expectedDefaultEvent, - eventTypeId: defaultPrefix + '.' + expectedDefaultEvent.eventTypeId, - key: coreSettings.key, - trafficTypeName: coreSettings.trafficType, - timestamp: event.timestamp, - }); // should track an event using the default mapper and key and traffic type from the SDK config - - /** Custom behavior: plugin options */ - - // init plugin with custom options - new SplitTracker(tracker, { mapper: customMapper, filter: customFilter, identities: customIdentities, prefix: '' }); - - // send hit and assert that it was properly tracked as a Split event - window.ga('send', hitSample); - event = (fakeStorage.events.track as jest.Mock).mock.calls.pop()[0]; - expect(event).toEqual( - { - ...customMapper(modelSample, defaultMapper(modelSample)), - key: customIdentities[0].key, - trafficTypeName: customIdentities[0].trafficType, - timestamp: event.timestamp, - }); // should track an event using a custom mapper and identity from the plugin options - - /** Custom behavior: SDK options */ - - // provide a new SplitTracker plugin with custom SDK options - GaToSplit({ - mapper: customMapper2, filter: customFilter, identities: customIdentities, prefix: '' - }, fakeParams as any); - // @ts-expect-error - [arg1, arg2, SplitTracker] = ga.mock.calls.pop(); - expect([arg1, arg2]).toEqual(['provide', 'splitTracker']); - expect(typeof SplitTracker === 'function').toBe(true); - - // init plugin - new SplitTracker(tracker); - - // send hit and assert that it was properly tracked as a Split event - window.ga('send', hitSample); - event = (fakeStorage.events.track as jest.Mock).mock.calls.pop()[0]; - expect(event).toEqual( - { - ...customMapper2(modelSample, defaultMapper(modelSample)), - key: customIdentities[0].key, - trafficTypeName: customIdentities[0].trafficType, - timestamp: event.timestamp, - }); // should track the event using a custom mapper and identity from the SDK options - - /** Custom behavior: SDK options, including a customMapper that set events key and traffic type */ - - // provide a new SplitTracker plugin with custom SDK options - GaToSplit({ - mapper: customMapper3, filter: customFilter, identities: customIdentities, prefix: '' - }, fakeParams as any); - // @ts-ignore - [arg1, arg2, SplitTracker] = ga.mock.calls.pop(); - expect([arg1, arg2]).toEqual(['provide', 'splitTracker']); - expect(typeof SplitTracker === 'function').toBe(true); - - // init plugin - new SplitTracker(tracker); - - // send hit and assert that it was properly tracked as a Split event - window.ga('send', hitSample); - event = (fakeStorage.events.track as jest.Mock).mock.calls.pop()[0]; - expect(event).toEqual( - { - ...customMapper3(modelSample, defaultMapper(modelSample)), - timestamp: event.timestamp, - }); // should track the event using a custom mapper and identity from the SDK options - - // test teardown - gaRemove(); -}); - -test('GaToSplit: `hits` flag param', () => { - - // test setup - const { ga, tracker } = gaMock(); - GaToSplit({}, fakeParams as any); // @ts-expect-error - let SplitTracker: any = ga.mock.calls.pop()[2]; - - // init plugin with custom options - new SplitTracker(tracker, { hits: false }); - - // send hit and assert that it was not tracked as a Split event - (fakeStorage.events.track as jest.Mock).mockClear(); - window.ga('send', hitSample); - expect(fakeStorage.events.track).toBeCalledTimes(0); - - // test teardown - gaRemove(); -}); - -test('GaToSplit: `autoRequire` script and flag param', () => { - // test setup - gaMock(); - loggerMock.error.mockClear(); - - // Create commands before autoRequire script is executed - window.ga('create', 'UA-ID-X', 'auto', 'tX'); - - GaToSplit({ autoRequire: true }, fakeParams as any); - expect(loggerMock.error).toBeCalledTimes(1); - - window.ga('create', 'UA-ID-Y', 'auto', 'tY'); - - // Run autoRequire iife - require('../autoRequire.js'); - - GaToSplit({ autoRequire: true }, fakeParams as any); - expect(loggerMock.error).toBeCalledTimes(1); - - // Assert auto-require script - window.ga('create', 'UA-ID-0'); - window.ga('create', 'UA-ID-1', 'auto', 't1'); - window.ga('create', 'UA-ID-2', { name: 't2' }); - window.ga('create', 'UA-ID-3', 'auto', { name: 't3' }); - window.ga('create', { trackingId: 'UA-ID-4', name: 't4' }); - - expect(window.ga.q.map(args => args[0])).toEqual([ - 'create' /* tX */, 'provide', - 'create' /* tY */, 'tX.require', - 'tY.require', 'provide', - 'create' /* default */, 'require', - 'create' /* t1 */, 't1.require', - 'create' /* t2 */, 't2.require', - 'create' /* t3 */, 't3.require', - 'create' /* t4 */, 't4.require', - ]); - - // test teardown - gaRemove(); -}); diff --git a/src/integrations/ga/__tests__/SplitToGa.spec.ts b/src/integrations/ga/__tests__/SplitToGa.spec.ts deleted file mode 100644 index d05e4bab..00000000 --- a/src/integrations/ga/__tests__/SplitToGa.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { SplitIO } from '../../../types'; -import { SPLIT_IMPRESSION, SPLIT_EVENT } from '../../../utils/constants'; - -// Mocks -import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { gaMock, gaRemove } from './gaMock'; - -// Test target -import { SplitToGa } from '../SplitToGa'; - -const fakeImpressionPayload: SplitIO.ImpressionData = { - impression: { - feature: 'hierarchical_splits_test', - keyName: 'nicolas@split.io', - treatment: 'on', - bucketingKey: undefined, - label: 'expected label', - time: 2000, - changeNumber: 1000, - }, - attributes: undefined, - ip: 'ip', - hostname: 'hostname', - sdkLanguageVersion: 'version', -}; -const fakeImpression: SplitIO.IntegrationData = { - type: SPLIT_IMPRESSION, - payload: fakeImpressionPayload, -}; -const defaultImpressionFieldsObject: UniversalAnalytics.FieldsObject = { - hitType: 'event', - eventCategory: 'split-impression', - eventAction: 'Evaluate ' + fakeImpressionPayload.impression.feature, - eventLabel: 'Treatment: ' + fakeImpressionPayload.impression.treatment + '. Targeting rule: ' + fakeImpressionPayload.impression.label + '.', - nonInteraction: true -}; - -const fakeEventPayload: SplitIO.EventData = { - eventTypeId: 'eventTypeId', - trafficTypeName: 'trafficTypeName', - value: 0, - timestamp: Date.now(), - key: 'key', - properties: {}, -}; -const fakeEvent: SplitIO.IntegrationData = { - type: SPLIT_EVENT, - payload: fakeEventPayload, -}; -const defaultEventFieldsObject = { - hitType: 'event', - eventCategory: 'split-event', - eventAction: fakeEventPayload.eventTypeId, - eventValue: fakeEventPayload.value, - nonInteraction: true -}; - -describe('SplitToGa', () => { - - test('SplitToGa.validateFieldsObject', () => { - expect(SplitToGa.validateFieldsObject(loggerMock, undefined)).toBe(false); - expect(SplitToGa.validateFieldsObject(loggerMock, null)).toBe(false); - expect(SplitToGa.validateFieldsObject(loggerMock, 123)).toBe(false); - expect(SplitToGa.validateFieldsObject(loggerMock, true)).toBe(false); - expect(SplitToGa.validateFieldsObject(loggerMock, 'something')).toBe(false); - expect(SplitToGa.validateFieldsObject(loggerMock, /asd/ig)).toBe(false); - expect(SplitToGa.validateFieldsObject(loggerMock, function () { })).toBe(false); - - expect(SplitToGa.validateFieldsObject(loggerMock, {})).toBe(false); // An empty object is an invalid FieldsObject instance - expect(SplitToGa.validateFieldsObject(loggerMock, { hitType: 10 })).toBe(true); // A fields object instance must have a HitType - expect(SplitToGa.validateFieldsObject(loggerMock, { hitType: 'event', ignoredProp: 'ignoredProp' })).toBe(true); // A fields object instance must have a HitType - }); - - test('SplitToGa.defaultMapper', () => { - // should return the corresponding FieldsObject for a given impression - expect(SplitToGa.defaultMapper(fakeImpression)).toEqual(defaultImpressionFieldsObject); - // should return the corresponding FieldsObject for a given event - expect(SplitToGa.defaultMapper(fakeEvent)).toEqual(defaultEventFieldsObject); - }); - - test('SplitToGa.getGa', () => { - loggerMock.mockClear(); - - const { ga } = gaMock(); - expect(SplitToGa.getGa()).toBe(ga); // should return ga command queue if it exists - - let integration = new SplitToGa(loggerMock, {}); - expect(typeof integration).toBe('object'); - expect(loggerMock.warn).not.toBeCalled(); - - gaRemove(); - expect(SplitToGa.getGa()).toBe(undefined); // should return undefined if ga command queue does not exist - - integration = new SplitToGa(loggerMock, {}); - expect(typeof integration).toBe('object'); // SplitToGa instances should be created even if ga command queue does not exist - // @ts-expect-error - integration.queue('fake-data'); - expect(loggerMock.warn.mock.calls).toEqual([ // Warn when creating and queueing while ga command queue does not exist - ['split-to-ga: `ga` command queue not found. No hits will be sent until it is available.'], - ['split-to-ga: `ga` command queue not found. No hit was sent.'] - ]); - }); - - test('SplitToGa (constructor and queue method)', () => { - - // test setup - const { ga } = gaMock(); - - /** Default behaviour **/ - const instance = new SplitToGa(loggerMock, {}) as SplitToGa; - instance.queue(fakeImpression); - // should queue `ga send` with the default mapped FieldsObject for impressions, appended with `splitHit` field - expect(ga).lastCalledWith('send', { ...defaultImpressionFieldsObject, splitHit: true }); - - instance.queue(fakeEvent); - // should queue `ga send` with the default mapped FieldsObject for events, appended with `splitHit` field - expect(ga).lastCalledWith('send', { ...defaultEventFieldsObject, splitHit: true }); - - expect(ga).toBeCalledTimes(2); - - /** Custom behaviour **/ - // Custom filter - function customFilter(data: SplitIO.IntegrationData) { - return data.type === SPLIT_EVENT; - } - // Custom mapper that returns a new FieldsObject instance - function customMapper() { - return { - hitType: 'event', - someField: 'someField', - } as UniversalAnalytics.FieldsObject; - } - const trackerNames = ['', 'namedTracker']; - const instance2 = new SplitToGa(loggerMock, { - filter: customFilter, - mapper: customMapper, - trackerNames, - }) as SplitToGa; - ga.mockClear(); - instance2.queue(fakeImpression); - expect(ga).not.toBeCalled(); // shouldn't queue `ga send` if a Split data (impression or event) is filtered - - instance2.queue(fakeEvent); - expect(ga.mock.calls).toEqual([ - ['send', { ...customMapper(), splitHit: true }], - [`${trackerNames[1]}.send`, { ...customMapper(), splitHit: true }] - ]); // should queue `ga send` with the custom trackerName and FieldsObject from customMapper, appended with `splitHit` field - - expect(ga).toBeCalledTimes(2); - - // Custom mapper that returns the default FieldsObject - function customMapper2(data: SplitIO.IntegrationData, defaultFieldsObject: UniversalAnalytics.FieldsObject) { - return defaultFieldsObject; - } - const instance3 = new SplitToGa(loggerMock, { - mapper: customMapper2, - }) as SplitToGa; - ga.mockClear(); - instance3.queue(fakeImpression); - // should queue `ga send` with the custom FieldsObject from customMapper2, appended with `splitHit` field - expect(ga).lastCalledWith('send', { ...customMapper2(fakeImpression, defaultImpressionFieldsObject), splitHit: true }); - - expect(ga).toBeCalledTimes(1); - - // Custom mapper that throws an error - function customMapper3() { - throw 'some error'; - } - const instance4 = new SplitToGa(loggerMock, { // @ts-expect-error - mapper: customMapper3, - }) as SplitToGa; - ga.mockClear(); - instance4.queue(fakeImpression); - expect(ga).not.toBeCalled(); // shouldn't queue `ga send` if a custom mapper throw an exception - - // `impressions` flags - const instance5 = new SplitToGa(loggerMock, { - impressions: false, - }) as SplitToGa; - ga.mockClear(); - instance5.queue(fakeImpression); - expect(ga).not.toBeCalled(); // shouldn't queue `ga send` for an impression if `impressions` flag is false - - // `impressions` flags - const instance6 = new SplitToGa(loggerMock, { - events: false, - }) as SplitToGa; - ga.mockClear(); - instance6.queue(fakeEvent); - expect(ga).not.toBeCalled(); // shouldn't queue `ga send` for a event if `events` flag is false - - // test teardown - gaRemove(); - }); -}); diff --git a/src/integrations/ga/__tests__/gaMock.ts b/src/integrations/ga/__tests__/gaMock.ts deleted file mode 100644 index 2a72863d..00000000 --- a/src/integrations/ga/__tests__/gaMock.ts +++ /dev/null @@ -1,60 +0,0 @@ -export function modelMock(fieldsObject: UniversalAnalytics.FieldsObject) { - return { - get(fieldName: string) { - return fieldsObject[fieldName as keyof UniversalAnalytics.FieldsObject]; - }, - set(fieldNameOrObject: string | {}, fieldValue?: any) { - if (typeof fieldNameOrObject === 'object') - fieldsObject = { ...fieldsObject, ...fieldNameOrObject }; - else - fieldsObject[fieldNameOrObject as keyof UniversalAnalytics.FieldsObject] = fieldValue; - } - }; -} - -export function gaMock() { - - const __originalSendHitTask = jest.fn(); - const __tasks: Record = { - sendHitTask: __originalSendHitTask - }; - const ga = jest.fn(function (command) { // @ts-ignore - (ga.q = ga.q || []).push(arguments); - - if (command === 'send') { - const fieldsObject = arguments[1]; - __tasks.sendHitTask(modelMock(fieldsObject)); - } - }); - - const set = jest.fn(function (taskName, taskFunc) { - __tasks[taskName] = taskFunc; - }); - const get = jest.fn(function (taskName) { - return __tasks[taskName]; - }); - - // Add ga to window object - if (typeof window === 'undefined') { // @ts-expect-error - if (global) global.window = {}; - } // @ts-expect-error - // eslint-disable-next-line no-undef - window['GoogleAnalyticsObject'] = 'ga'; - // eslint-disable-next-line no-undef - window['ga'] = window['ga'] || ga; - - return { - ga, - tracker: { - get, - set, - __originalSendHitTask, - } - }; -} - -export function gaRemove() { - if (typeof window !== 'undefined') // @ts-expect-error - // eslint-disable-next-line no-undef - window[window['GoogleAnalyticsObject'] || 'ga'] = undefined; -} diff --git a/src/integrations/ga/autoRequire.js b/src/integrations/ga/autoRequire.js deleted file mode 100644 index a6adad72..00000000 --- a/src/integrations/ga/autoRequire.js +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable no-undef */ -/** - * Auto-require script to use with GoogleAnalyticsToSplit integration - */ -(function (w, g, o) { - w[o] = w[o] || g; - w[g] = w[g] || function () { w[g].q.push(arguments); }; - w[g].q = w[g].q || []; - - var trackerNames = {}; - function name(arg) { return typeof arg === 'object' && typeof arg.name === 'string' && arg.name; } - - function processCommand(command) { // Queue a `require` command if v is a `create` command - if (command && command[0] === 'create') { - var trackerName = name(command[1]) || name(command[2]) || name(command[3]) || (typeof command[3] === 'string' ? command[3] : undefined); // Get tracker name - - if (!trackerNames[trackerName]) { - trackerNames[trackerName] = true; - w[g]((trackerName ? trackerName + '.' : '') + 'require', 'splitTracker'); // Auto-require - } - } - } - - w[g].q.forEach(processCommand); // Process already queued commands - - var originalPush = w[g].q.push; - w[g].q.push = function (command) { // Spy new queued commands - var result = originalPush.apply(this, arguments); - processCommand(command); - return result; - }; - -})(window, 'ga', 'GoogleAnalyticsObject'); diff --git a/src/integrations/ga/types.ts b/src/integrations/ga/types.ts deleted file mode 100644 index dfa5f11e..00000000 --- a/src/integrations/ga/types.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { SplitIO } from '../../types'; - -/** - * A pair of user key and it's trafficType, required for tracking valid Split events. - * @typedef {Object} Identity - * @property {string} key The user key. - * @property {string} trafficType The key traffic type. - */ -export type Identity = { - key: string; - trafficType: string; -}; - -/** - * Options for GoogleAnalyticsToSplit integration plugin - */ -export interface GoogleAnalyticsToSplitOptions { - /** - * Optional flag to filter GA hits from being tracked as Split events. - * @property {boolean} hits - * @default true - */ - hits?: boolean, - /** - * Optional predicate used to define a custom filter for tracking GA hits as Split events. - * For example, the following filter allows to track only 'event' hits: - * `(model) => model.get('hitType') === 'event'` - * By default, all hits are tracked as Split events. - */ - filter?: (model: UniversalAnalytics.Model) => boolean, - /** - * Optional function useful when you need to modify the Split event before tracking it. - * This function is invoked with two arguments: - * 1. the GA model object representing the hit. - * 2. the default format of the mapped Split event instance. - * The return value must be a Split event, that can be the second argument or a new object. - * - * For example, the following mapper adds a custom property to events: - * `(model, defaultMapping) => { - * defaultMapping.properties.someProperty = SOME_VALUE; - * return defaultMapping; - * }` - */ - mapper?: (model: UniversalAnalytics.Model, defaultMapping: SplitIO.EventData) => SplitIO.EventData, - /** - * Optional prefix for EventTypeId, to prevent any kind of data collision between events. - * @property {string} prefix - * @default 'ga' - */ - prefix?: string, - /** - * List of Split identities (key & traffic type pairs) used to track events. - * If not provided, events are sent using the key and traffic type provided at SDK config - */ - identities?: Identity[], - /** - * Optional flag to log an error if the `auto-require` script is not detected. - * The auto-require script automatically requires the `splitTracker` plugin for created trackers, - * and should be placed right after your Google Analytics, Google Tag Manager or gtag.js script tag. - * - * @see {@link https://help.split.io/hc/en-us/articles/360040838752#set-up-with-gtm-and-gtag.js} - * - * @property {boolean} autoRequire - * @default false - */ - autoRequire?: boolean, -} - -/** - * Enable 'Google Analytics to Split' integration, to track Google Analytics hits as Split events. - * Used by the browser variant of the isomorphic JS SDK. - * - * @see {@link https://help.split.io/hc/en-us/articles/360040838752#google-analytics-to-split} - */ -export interface IGoogleAnalyticsToSplitConfig extends GoogleAnalyticsToSplitOptions { - type: 'GOOGLE_ANALYTICS_TO_SPLIT' -} - -/** - * Options for SplitToGoogleAnalytics integration plugin - */ -export interface SplitToGoogleAnalyticsOptions { - /** - * Optional flag to filter Split impressions from being tracked as GA hits. - * @property {boolean} impressions - * @default true - */ - impressions?: boolean, - /** - * Optional flag to filter Split events from being tracked as GA hits. - * @property {boolean} events - * @default true - */ - events?: boolean, - /** - * Optional predicate used to define a custom filter for tracking Split data (events and impressions) as GA hits. - * For example, the following filter allows to track only impressions, equivalent to setting events to false: - * `(data) => data.type === 'IMPRESSION'` - */ - filter?: (data: SplitIO.IntegrationData) => boolean, - /** - * Optional function useful when you need to modify the GA hit before sending it. - * This function is invoked with two arguments: - * 1. the input data (Split event or impression). - * 2. the default format of the mapped FieldsObject instance (GA hit). - * The return value must be a FieldsObject, that can be the second argument or a new object. - * - * For example, the following mapper adds a custom dimension to hits: - * `(data, defaultMapping) => { - * defaultMapping.dimension1 = SOME_VALUE; - * return defaultMapping; - * }` - * - * Default FieldsObject instance for data.type === 'IMPRESSION': - * `{ - * hitType: 'event', - * eventCategory: 'split-impression', - * eventAction: 'Evaluate ' + data.payload.impression.feature, - * eventLabel: 'Treatment: ' + data.payload.impression.treatment + '. Targeting rule: ' + data.payload.impression.label + '.', - * nonInteraction: true, - * }` - * Default FieldsObject instance for data.type === 'EVENT': - * `{ - * hitType: 'event', - * eventCategory: 'split-event', - * eventAction: data.payload.eventTypeId, - * eventValue: data.payload.value, - * nonInteraction: true, - * }` - */ - mapper?: (data: SplitIO.IntegrationData, defaultMapping: UniversalAnalytics.FieldsObject) => UniversalAnalytics.FieldsObject, - /** - * List of tracker names to send the hit. An empty string represents the default tracker. - * If not provided, hits are only sent to default tracker. - */ - trackerNames?: string[], -} - -/** - * Enable 'Split to Google Analytics' integration, to track Split impressions and events as Google Analytics hits. - * Used by the browser variant of the isomorphic JS SDK. - * - * @see {@link https://help.split.io/hc/en-us/articles/360040838752#split-to-google-analytics} - */ -export interface ISplitToGoogleAnalyticsConfig extends SplitToGoogleAnalyticsOptions { - type: 'SPLIT_TO_GOOGLE_ANALYTICS' -} - -/** - * Available integration options for the browser - * Used by the browser variant of the isomorphic JS SDK. - */ -export type BrowserIntegration = ISplitToGoogleAnalyticsConfig | IGoogleAnalyticsToSplitConfig; diff --git a/src/utils/constants/browser.ts b/src/utils/constants/browser.ts index ec2add2c..d627f780 100644 --- a/src/utils/constants/browser.ts +++ b/src/utils/constants/browser.ts @@ -1,6 +1,2 @@ -// Integration types -export const GOOGLE_ANALYTICS_TO_SPLIT = 'GOOGLE_ANALYTICS_TO_SPLIT'; -export const SPLIT_TO_GOOGLE_ANALYTICS = 'SPLIT_TO_GOOGLE_ANALYTICS'; - // This value might be eventually set via a config parameter export const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days diff --git a/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts b/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts index a9b76aa1..3de62641 100644 --- a/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts +++ b/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts @@ -16,8 +16,8 @@ describe('integrations validator for pluggable integrations', () => { }); test('Filters invalid integration factories from `integrations` array', () => { - const validNoopIntFactory = () => { }; // no-op integration, such as GoogleAnalyticsToSplit - const validIntFactory = () => { return { queue() { } }; }; // integration with queue handler, such as SplitToGoogleAnalytics + const validNoopIntFactory = () => { }; // integration with no queue handler, such as 3rdPartyAnalyticsToSplit + const validIntFactory = () => { return { queue() { } }; }; // integration with queue handler, such as SplitTo3rdPartyAnalytics const invalid = { queue() { } }; // Integration factories that are invalid objects are removed