diff --git a/.agents/knowledge/entry-points.md b/.agents/knowledge/entry-points.md index 2d5389772..c9eedd583 100644 --- a/.agents/knowledge/entry-points.md +++ b/.agents/knowledge/entry-points.md @@ -16,3 +16,8 @@ - `packages/analytics-js/src/components/core/Analytics.ts` — v3 lifecycle engine, service composition, and initialization sequence (`packages/analytics-js/src/components/core/Analytics.ts::Analytics`). - `packages/analytics-v1.1/src/core/analytics.js` — legacy line core implementation (class-centric runtime still shipped as `rudder-sdk-js`) (`packages/analytics-v1.1/src/core/analytics.js::Analytics`). - `packages/loading-scripts/src/index.ts` — snippet loader that buffers calls and chooses modern vs legacy runtime script dynamically (`packages/loading-scripts/src/index.ts:1`). + +## SDK-5014 — Amplitude V2 Autocapture Entry Points + +- Amplitude device-mode integration work lives under `packages/analytics-js-integrations/src/integrations/Amplitude/`; for v2 autocapture changes, start with `browser.js` for initialization, `utils.js` for helper getters, and `constants.js` for integration constants. +- Existing Jest coverage for Amplitude Browser SDK v2 initialization payload shape is in `packages/analytics-js-integrations/__tests__/integrations/Amplitude/browser.test.js`; helper behavior is covered in `packages/analytics-js-integrations/__tests__/integrations/Amplitude/util.test.js`. diff --git a/.agents/knowledge/patterns.md b/.agents/knowledge/patterns.md index 98a2fa94a..026a5b557 100644 --- a/.agents/knowledge/patterns.md +++ b/.agents/knowledge/patterns.md @@ -17,3 +17,8 @@ - Plugin calls prefer extension-point invocation (`invokeSingle`/`invokeMultiple`) to keep queue and destination behaviors swappable (`packages/analytics-js/src/services/PluginEngine/PluginEngine.ts::PluginEngine.invokeSingle`). - Load-time buffering is a consistent behavior: preload snippet arrays and in-memory queues are drained after initialization to preserve call order (`packages/loading-scripts/src/index.ts:12`, `packages/analytics-js/src/components/core/Analytics.ts::Analytics.processDataInPreloadBuffer`). - Legacy v1.1 still follows class-centric mutable state and explicit polling for integration readiness (`allModulesInitialized` Promise loop), unlike v3 reactive effects (`packages/analytics-v1.1/src/core/analytics.js::Analytics.allModulesInitialized`). + +## SDK-5014 — Amplitude V2 Autocapture Config Boundaries + +- Amplitude Browser SDK v2 autocapture config is assembled in `packages/analytics-js-integrations/src/integrations/Amplitude/browser.js:init()` using helper getters from `packages/analytics-js-integrations/src/integrations/Amplitude/utils.js`. +- The dedicated `pageViews` config key maps only to the Amplitude SDK `autocapture.pageViews` option; it intentionally does not affect Rudder `page()` translation gates such as `trackAllPages`, `trackCategorizedPages`, or `trackNamedPages`. diff --git a/package-lock.json b/package-lock.json index 04fc3f5e8..668721dcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14578,16 +14578,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -15365,9 +15365,9 @@ "license": "ISC" }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -22270,6 +22270,23 @@ } } }, + "node_modules/nx/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/nx/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/packages/analytics-js-integrations/__tests__/integrations/Amplitude/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/Amplitude/browser.test.js index 4716db813..c1a4193e4 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/Amplitude/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/Amplitude/browser.test.js @@ -66,6 +66,19 @@ const destinationInfo = { destinationId: 'sample-destination-id', }; +const defaultV2Autocapture = { + attribution: true, + pageViews: false, + sessions: false, + formInteractions: false, + fileDownloads: false, + elementInteractions: false, + frustrationInteractions: false, + networkTracking: false, + webVitals: false, + pageUrlEnrichment: false, +}; + describe('Amplitude', () => { beforeAll(() => { // Add a dummy script as it is required by the init script @@ -277,18 +290,7 @@ describe('Amplitude', () => { expect(window.amplitude._q[0].name).toBe('init'); expect(window.amplitude._q[0].args[1]).toBeUndefined(); expect(window.amplitude._q[0].args[2]).toMatchObject({ - autocapture: { - attribution: true, - pageViews: false, - sessions: false, - formInteractions: false, - fileDownloads: false, - elementInteractions: false, - frustrationInteractions: false, - networkTracking: false, - webVitals: false, - pageUrlEnrichment: false, - }, + autocapture: defaultV2Autocapture, serverUrl: 'https://some.proxyserverurl.com', }); expect(window.amplitude._q[0].args[2].attribution).toBeUndefined(); @@ -309,22 +311,71 @@ describe('Amplitude', () => { expect(document.querySelector(`script[src="${AMPLITUDE_V2_SDK_URL}"]`)).toBeTruthy(); expect(window.amplitude._q[0].args[2]).toMatchObject({ autocapture: { + ...defaultV2Autocapture, attribution: false, - pageViews: false, - sessions: false, - formInteractions: false, - fileDownloads: false, - elementInteractions: false, - frustrationInteractions: false, - networkTracking: false, - webVitals: false, - pageUrlEnrichment: false, }, serverUrl: 'https://some.proxyserverurl.com', }); expect(window.amplitude._q[0].args[2].attribution).toBeUndefined(); }); + it.each([ + ['pageViews', 'pageViews', true], + ['pageUrlEnrichment', 'pageUrlEnrichment', true], + ['trackSessionEvents', 'sessions', true], + ['webVitals', 'webVitals', true], + ['fileDownloads', 'fileDownloads', true], + ['frustrationInteractions', 'frustrationInteractions', true], + ['networkTracking', 'networkTracking', true], + ['elementInteractions', 'elementInteractions', true], + ['formInteractions', 'formInteractions', true], + ])( + 'should map %s config to only autocapture.%s for v2', + (configKey, autocaptureKey, configValue) => { + const amplitude = new Amplitude( + { + ...destinationConfig, + sdkVersion: 2, + attribution: false, + [configKey]: configValue, + }, + analyticsInstance, + destinationInfo, + ); + amplitude.init(); + + expect(window.amplitude._q[0].args[2].autocapture).toStrictEqual({ + ...defaultV2Autocapture, + [autocaptureKey]: true, + }); + }, + ); + + it('should keep auto-capture page views independent from user-instrumented page toggles', () => { + const amplitude = new Amplitude( + { + ...destinationConfig, + sdkVersion: 2, + attribution: false, + pageViews: true, + trackAllPages: false, + trackCategorizedPages: false, + trackNamedPages: false, + }, + analyticsInstance, + destinationInfo, + ); + amplitude.init(); + + expect(window.amplitude._q[0].args[2].autocapture).toStrictEqual({ + ...defaultV2Autocapture, + pageViews: true, + }); + expect(amplitude.trackAllPages).toBe(false); + expect(amplitude.trackCategorizedPages).toBe(false); + expect(amplitude.trackNamedPages).toBe(false); + }); + it('should set the correct SRI integrity on the injected v1 script', () => { const amplitude = new Amplitude( { ...destinationConfig, sdkVersion: 1 }, @@ -1075,6 +1126,39 @@ describe('Amplitude', () => { expect(spy).toHaveBeenCalledWith('Viewed page NAME', {}); }); + + it('should not translate page calls when only v2 auto-capture page views is enabled', () => { + const config = { + apiKey: 'YOUR_AMPLITUDE_API_KEY', + sdkVersion: 2, + pageViews: true, + trackAllPages: false, + trackCategorizedPages: false, + trackNamedPages: false, + }; + + const analytics = { + logLevel: 'debug', + getAnonymousId: () => 'ANONYMOUS_ID', + }; + + const rudderElement = { + message: { + properties: {}, + name: 'NAME', + category: 'CATEGORY', + integrations: {}, + }, + }; + + const amplitude = new Amplitude(config, analytics, destinationInfo); + window.amplitude = window.amplitude ?? { track: jest.fn() }; + const spy = jest.spyOn(window.amplitude, 'track'); + + amplitude.page(rudderElement); + + expect(spy).not.toHaveBeenCalled(); + }); }); describe('group', () => { diff --git a/packages/analytics-js-integrations/__tests__/integrations/Amplitude/util.test.js b/packages/analytics-js-integrations/__tests__/integrations/Amplitude/util.test.js index ecba4f0ba..3e17112ec 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/Amplitude/util.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/Amplitude/util.test.js @@ -5,6 +5,15 @@ import { getDestinationOptions, formatUrl, getAmplitudeSdkVersion, + getAutoCapturePageViews, + getPageUrlEnrichment, + getTrackSessionEvents, + getWebVitals, + getFileDownloads, + getFrustrationInteractions, + getNetworkTracking, + getElementInteractions, + getFormInteractions, } from '../../../src/integrations/Amplitude/utils'; describe('getTraitsToSetOnce', () => { @@ -210,3 +219,35 @@ describe('getAmplitudeSdkVersion', () => { expect(getAmplitudeSdkVersion({ sdkVersion: '2' })).toBe(2); }); }); + +describe('Amplitude v2 autocapture config helpers', () => { + const helperCases = [ + { + name: 'getAutoCapturePageViews', + getter: getAutoCapturePageViews, + key: 'pageViews', + }, + { name: 'getPageUrlEnrichment', getter: getPageUrlEnrichment, key: 'pageUrlEnrichment' }, + { name: 'getWebVitals', getter: getWebVitals, key: 'webVitals' }, + { name: 'getFileDownloads', getter: getFileDownloads, key: 'fileDownloads' }, + { + name: 'getFrustrationInteractions', + getter: getFrustrationInteractions, + key: 'frustrationInteractions', + }, + { name: 'getNetworkTracking', getter: getNetworkTracking, key: 'networkTracking' }, + { name: 'getElementInteractions', getter: getElementInteractions, key: 'elementInteractions' }, + { name: 'getFormInteractions', getter: getFormInteractions, key: 'formInteractions' }, + { name: 'getTrackSessionEvents', getter: getTrackSessionEvents, key: 'trackSessionEvents' }, + ]; + + it.each(helperCases)('$name should default missing config to false', ({ getter }) => { + expect(getter({})).toBe(false); + }); + + it.each(helperCases)('$name should read true and false config values', ({ getter, key }) => { + expect(getter({ [key]: true })).toBe(true); + expect(getter({ [key]: false })).toBe(false); + }); + +}); diff --git a/packages/analytics-js-integrations/src/integrations/Amplitude/browser.js b/packages/analytics-js-integrations/src/integrations/Amplitude/browser.js index dd3894080..05250ec02 100644 --- a/packages/analytics-js-integrations/src/integrations/Amplitude/browser.js +++ b/packages/analytics-js-integrations/src/integrations/Amplitude/browser.js @@ -11,6 +11,15 @@ import { getFieldsToUnset, formatUrl, getAmplitudeSdkVersion, + getAutoCapturePageViews, + getPageUrlEnrichment, + getTrackSessionEvents, + getWebVitals, + getFileDownloads, + getFrustrationInteractions, + getNetworkTracking, + getElementInteractions, + getFormInteractions, } from './utils'; import { getValueOrDefault } from '../../utils/utils'; @@ -44,6 +53,15 @@ class Amplitude { this.groupTypeTrait = config.groupTypeTrait; this.groupValueTrait = config.groupValueTrait; this.sdkVersion = getAmplitudeSdkVersion(config); + this.autoCapturePageViews = getAutoCapturePageViews(config); + this.pageUrlEnrichment = getPageUrlEnrichment(config); + this.trackSessionEvents = getTrackSessionEvents(config); + this.webVitals = getWebVitals(config); + this.fileDownloads = getFileDownloads(config); + this.frustrationInteractions = getFrustrationInteractions(config); + this.networkTracking = getNetworkTracking(config); + this.elementInteractions = getElementInteractions(config); + this.formInteractions = getFormInteractions(config); ({ shouldApplyDeviceModeTransformation: this.shouldApplyDeviceModeTransformation, propagateEventsUntransformedOnError: this.propagateEventsUntransformedOnError, @@ -69,18 +87,17 @@ class Amplitude { this.sdkVersion === AMPLITUDE_SDK_V2 ? { ...commonInitOptions, - // Enable only attribution in v2; all other autocapture toggles stay off. autocapture: { attribution: !this.attribution, - pageViews: false, - sessions: false, - formInteractions: false, - fileDownloads: false, - elementInteractions: false, - frustrationInteractions: false, - networkTracking: false, - webVitals: false, - pageUrlEnrichment: false, + pageViews: this.autoCapturePageViews, + sessions: this.trackSessionEvents, + formInteractions: this.formInteractions, + fileDownloads: this.fileDownloads, + elementInteractions: this.elementInteractions, + frustrationInteractions: this.frustrationInteractions, + networkTracking: this.networkTracking, + webVitals: this.webVitals, + pageUrlEnrichment: this.pageUrlEnrichment, }, } : { diff --git a/packages/analytics-js-integrations/src/integrations/Amplitude/constants.js b/packages/analytics-js-integrations/src/integrations/Amplitude/constants.js index 36c59c6dc..b68d2ca0f 100644 --- a/packages/analytics-js-integrations/src/integrations/Amplitude/constants.js +++ b/packages/analytics-js-integrations/src/integrations/Amplitude/constants.js @@ -2,6 +2,15 @@ import { AM_NAME as NAME, AM_DISPLAY_NAME as DISPLAY_NAME } from '../../constant const DIR_NAME = 'Amplitude'; const AMPLITUDE_SDK_VERSION_CONFIG_KEY = 'sdkVersion'; +const AMPLITUDE_AUTO_CAPTURE_PAGE_VIEWS_CONFIG_KEY = 'pageViews'; +const AMPLITUDE_PAGE_URL_ENRICHMENT_CONFIG_KEY = 'pageUrlEnrichment'; +const AMPLITUDE_TRACK_SESSION_EVENTS_CONFIG_KEY = 'trackSessionEvents'; +const AMPLITUDE_WEB_VITALS_CONFIG_KEY = 'webVitals'; +const AMPLITUDE_FILE_DOWNLOADS_CONFIG_KEY = 'fileDownloads'; +const AMPLITUDE_FRUSTRATION_INTERACTIONS_CONFIG_KEY = 'frustrationInteractions'; +const AMPLITUDE_NETWORK_TRACKING_CONFIG_KEY = 'networkTracking'; +const AMPLITUDE_ELEMENT_INTERACTIONS_CONFIG_KEY = 'elementInteractions'; +const AMPLITUDE_FORM_INTERACTIONS_CONFIG_KEY = 'formInteractions'; const AMPLITUDE_SDK_V1 = 1; const AMPLITUDE_SDK_V2 = 2; @@ -18,6 +27,15 @@ export { DISPLAY_NAME, DIR_NAME, AMPLITUDE_SDK_VERSION_CONFIG_KEY, + AMPLITUDE_AUTO_CAPTURE_PAGE_VIEWS_CONFIG_KEY, + AMPLITUDE_PAGE_URL_ENRICHMENT_CONFIG_KEY, + AMPLITUDE_TRACK_SESSION_EVENTS_CONFIG_KEY, + AMPLITUDE_WEB_VITALS_CONFIG_KEY, + AMPLITUDE_FILE_DOWNLOADS_CONFIG_KEY, + AMPLITUDE_FRUSTRATION_INTERACTIONS_CONFIG_KEY, + AMPLITUDE_NETWORK_TRACKING_CONFIG_KEY, + AMPLITUDE_ELEMENT_INTERACTIONS_CONFIG_KEY, + AMPLITUDE_FORM_INTERACTIONS_CONFIG_KEY, AMPLITUDE_SDK_V1, AMPLITUDE_SDK_V2, }; diff --git a/packages/analytics-js-integrations/src/integrations/Amplitude/utils.js b/packages/analytics-js-integrations/src/integrations/Amplitude/utils.js index cd5d3d69d..08b97440f 100644 --- a/packages/analytics-js-integrations/src/integrations/Amplitude/utils.js +++ b/packages/analytics-js-integrations/src/integrations/Amplitude/utils.js @@ -4,6 +4,15 @@ import { AMPLITUDE_SDK_V1, AMPLITUDE_SDK_V2, AMPLITUDE_SDK_VERSION_CONFIG_KEY, + AMPLITUDE_AUTO_CAPTURE_PAGE_VIEWS_CONFIG_KEY, + AMPLITUDE_PAGE_URL_ENRICHMENT_CONFIG_KEY, + AMPLITUDE_TRACK_SESSION_EVENTS_CONFIG_KEY, + AMPLITUDE_WEB_VITALS_CONFIG_KEY, + AMPLITUDE_FILE_DOWNLOADS_CONFIG_KEY, + AMPLITUDE_FRUSTRATION_INTERACTIONS_CONFIG_KEY, + AMPLITUDE_NETWORK_TRACKING_CONFIG_KEY, + AMPLITUDE_ELEMENT_INTERACTIONS_CONFIG_KEY, + AMPLITUDE_FORM_INTERACTIONS_CONFIG_KEY, } from './constants'; const getTraitsToSetOnce = config => { @@ -74,6 +83,34 @@ const getAmplitudeSdkVersion = config => { : AMPLITUDE_SDK_V1; }; +const getBooleanConfigValue = (config, key) => config?.[key] === true; + +const getAutoCapturePageViews = config => + getBooleanConfigValue(config, AMPLITUDE_AUTO_CAPTURE_PAGE_VIEWS_CONFIG_KEY); + +const getPageUrlEnrichment = config => + getBooleanConfigValue(config, AMPLITUDE_PAGE_URL_ENRICHMENT_CONFIG_KEY); + +const getTrackSessionEvents = config => + getBooleanConfigValue(config, AMPLITUDE_TRACK_SESSION_EVENTS_CONFIG_KEY); + +const getWebVitals = config => getBooleanConfigValue(config, AMPLITUDE_WEB_VITALS_CONFIG_KEY); + +const getFileDownloads = config => + getBooleanConfigValue(config, AMPLITUDE_FILE_DOWNLOADS_CONFIG_KEY); + +const getFrustrationInteractions = config => + getBooleanConfigValue(config, AMPLITUDE_FRUSTRATION_INTERACTIONS_CONFIG_KEY); + +const getNetworkTracking = config => + getBooleanConfigValue(config, AMPLITUDE_NETWORK_TRACKING_CONFIG_KEY); + +const getElementInteractions = config => + getBooleanConfigValue(config, AMPLITUDE_ELEMENT_INTERACTIONS_CONFIG_KEY); + +const getFormInteractions = config => + getBooleanConfigValue(config, AMPLITUDE_FORM_INTERACTIONS_CONFIG_KEY); + export { getTraitsToSetOnce, getTraitsToIncrement, @@ -81,4 +118,13 @@ export { getFieldsToUnset, formatUrl, getAmplitudeSdkVersion, + getAutoCapturePageViews, + getPageUrlEnrichment, + getTrackSessionEvents, + getWebVitals, + getFileDownloads, + getFrustrationInteractions, + getNetworkTracking, + getElementInteractions, + getFormInteractions, };