diff --git a/packages/analytics-browser/src/attribution/helpers.ts b/packages/analytics-browser/src/attribution/helpers.ts index 6535f1223..377799026 100644 --- a/packages/analytics-browser/src/attribution/helpers.ts +++ b/packages/analytics-browser/src/attribution/helpers.ts @@ -1,7 +1,17 @@ -import { createIdentifyEvent, Identify, ILogger, Campaign, BASE_CAMPAIGN } from '@amplitude/analytics-core'; +import { + createIdentifyEvent, + Identify, + ILogger, + Campaign, + BASE_CAMPAIGN, + getGlobalScope, +} from '@amplitude/analytics-core'; + +import { ExcludeInternalReferrersOptions, EXCLUDE_INTERNAL_REFERRERS_CONDITIONS } from '../types'; export interface Options { excludeReferrers?: (string | RegExp)[]; + excludeInternalReferrers?: true | false | ExcludeInternalReferrersOptions; initialEmptyValue?: string; resetSessionOnNewCampaign?: boolean; optOut?: boolean; @@ -22,16 +32,41 @@ const isDirectTraffic = (current: Campaign) => { return Object.values(current).every((value) => !value); }; +const isEmptyCampaign = (campaign: Campaign) => { + const campaignWithoutReferrer = { ...campaign, referring_domain: undefined, referrer: undefined }; + return Object.values(campaignWithoutReferrer).every((value) => !value); +}; + export const isNewCampaign = ( current: Campaign, previous: Campaign | undefined, options: Options, logger: ILogger, isNewSession = true, + topLevelDomain?: string, ) => { const { referrer, referring_domain, ...currentCampaign } = current; const { referrer: _previous_referrer, referring_domain: prevReferringDomain, ...previousCampaign } = previous || {}; + const { excludeInternalReferrers } = options; + + if (excludeInternalReferrers) { + const condition = getExcludeInternalReferrersCondition(excludeInternalReferrers, logger); + if ( + !(condition instanceof TypeError) && + current.referring_domain && + isInternalReferrer(current.referring_domain, topLevelDomain) + ) { + if (condition === 'always') { + debugLogInternalReferrerExclude(condition, current.referring_domain, logger); + return false; + } else if (condition === 'ifEmptyCampaign' && isEmptyCampaign(current)) { + debugLogInternalReferrerExclude(condition, current.referring_domain, logger); + return false; + } + } + } + if (isExcludedReferrer(options.excludeReferrers, current.referring_domain)) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions logger.debug(`This is not a new campaign because ${current.referring_domain} is in the exclude referrer list.`); @@ -65,6 +100,13 @@ export const isExcludedReferrer = (excludeReferrers: (string | RegExp)[] = [], r ); }; +export const isSubdomainOf = (subDomain: string, domain: string) => { + const cookieDomainWithLeadingDot = domain.startsWith('.') ? domain : `.${domain}`; + const subDomainWithLeadingDot = subDomain.startsWith('.') ? subDomain : `.${subDomain}`; + if (subDomainWithLeadingDot.endsWith(cookieDomainWithLeadingDot)) return true; + return false; +}; + export const createCampaignEvent = (campaign: Campaign, options: Options) => { const campaignParameters: Campaign = { // This object definition allows undefined keys to be iterated on @@ -93,3 +135,104 @@ export const getDefaultExcludedReferrers = (cookieDomain: string | undefined) => } return []; }; + +/** + * Parses the excludeInternalReferrers configuration to determine the condition on which to + * exclude internal referrers for campaign attribution. + * + * If the config is invalid type, log and return a TypeError. + * + * (this does explicit type checking so don't have to rely on TS compiler to catch invalid types) + * + * @param excludeInternalReferrers - attribution.excludeInternalReferrers configuration + * @param logger - logger instance to log error when TypeError + * @returns The condition if the config is valid, TypeError if the config is invalid. + */ +const getExcludeInternalReferrersCondition = ( + excludeInternalReferrers: ExcludeInternalReferrersOptions | boolean, + logger: ILogger, +): ExcludeInternalReferrersOptions['condition'] | TypeError => { + if (excludeInternalReferrers === true) { + return EXCLUDE_INTERNAL_REFERRERS_CONDITIONS.always; + } + if (typeof excludeInternalReferrers === 'object') { + const { condition } = excludeInternalReferrers; + if (typeof condition === 'string' && Object.keys(EXCLUDE_INTERNAL_REFERRERS_CONDITIONS).includes(condition)) { + return condition; + } else if (typeof condition === 'undefined') { + return EXCLUDE_INTERNAL_REFERRERS_CONDITIONS.always; + } + } + const errorMessage = `Invalid configuration provided for attribution.excludeInternalReferrers: ${JSON.stringify( + excludeInternalReferrers, + )}`; + logger.error(errorMessage); + return new TypeError(errorMessage); +}; + +// helper function to log debug message when internal referrer is excluded +// (added this to prevent code duplication and improve readability) +function debugLogInternalReferrerExclude( + condition: ExcludeInternalReferrersOptions['condition'], + referringDomain: string, + logger: ILogger, +) { + const baseMessage = `This is not a new campaign because referring_domain=${referringDomain} is on the same domain as the current page and it is configured to exclude internal referrers`; + if (condition === 'always') { + logger.debug(baseMessage); + } else if (condition === 'ifEmptyCampaign') { + logger.debug(`${baseMessage} with empty campaign parameters`); + } +} + +const KNOWN_2LDS = [ + 'co.uk', + 'gov.uk', + 'ac.uk', + 'co.jp', + 'ne.jp', + 'or.jp', + 'co.kr', + 'or.kr', + 'go.kr', + 'com.au', + 'net.au', + 'org.au', + 'com.br', + 'net.br', + 'org.br', + 'com.cn', + 'net.cn', + 'org.cn', + 'com.mx', + 'github.io', + 'gitlab.io', + 'cloudfront.net', + 'herokuapp.com', + 'appspot.com', + 'azurewebsites.net', + 'firebaseapp.com', +]; + +export const getDomain = (hostname: string) => { + const parts = hostname.split('.'); + let tld = parts[parts.length - 1]; + let name = parts[parts.length - 2]; + if (KNOWN_2LDS.find((tld) => hostname.endsWith(`.${tld}`))) { + tld = parts[parts.length - 2] + '.' + parts[parts.length - 1]; + name = parts[parts.length - 3]; + } + + if (!name) return tld; + + return `${name}.${tld}`; +}; + +const isInternalReferrer = (referringDomain: string, topLevelDomain?: string) => { + const globalScope = getGlobalScope(); + /* istanbul ignore if */ + if (!globalScope) return false; + // if referring domain is subdomain of config.cookieDomain, return true + const internalDomain = (topLevelDomain || '').trim() || getDomain(globalScope.location.hostname); + return isSubdomainOf(referringDomain, internalDomain); +}; diff --git a/packages/analytics-browser/src/attribution/web-attribution.ts b/packages/analytics-browser/src/attribution/web-attribution.ts index 3cbf67bd1..928ce59ae 100644 --- a/packages/analytics-browser/src/attribution/web-attribution.ts +++ b/packages/analytics-browser/src/attribution/web-attribution.ts @@ -21,7 +21,7 @@ export class WebAttribution { sessionTimeout: number; lastEventTime?: number; logger: ILogger; - + topLevelDomain?: string; constructor(options: Options, config: BrowserConfig) { this.options = { initialEmptyValue: 'EMPTY', @@ -37,6 +37,7 @@ export class WebAttribution { this.sessionTimeout = config.sessionTimeout; this.lastEventTime = config.lastEventTime; this.logger = config.loggerProvider; + this.topLevelDomain = config.topLevelDomain; config.loggerProvider.log('Installing web attribution tracking.'); } @@ -49,7 +50,16 @@ export class WebAttribution { [this.currentCampaign, this.previousCampaign] = await this.fetchCampaign(); const isEventInNewSession = !this.lastEventTime ? true : isNewSession(this.sessionTimeout, this.lastEventTime); - if (isNewCampaign(this.currentCampaign, this.previousCampaign, this.options, this.logger, isEventInNewSession)) { + if ( + isNewCampaign( + this.currentCampaign, + this.previousCampaign, + this.options, + this.logger, + isEventInNewSession, + this.topLevelDomain, + ) + ) { this.shouldTrackNewCampaign = true; await this.storage.set(this.storageKey, this.currentCampaign); } diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 690a635a1..97b6a9101 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -107,6 +107,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public diagnosticsSampleRate: number = 0, public diagnosticsClient?: IDiagnosticsClient, public remoteConfig?: RemoteConfigOptions, + public topLevelDomain?: string, ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); this._cookieStorage = cookieStorage; @@ -134,6 +135,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { this.remoteConfig = this.remoteConfig || {}; this.remoteConfig.fetchRemoteConfig = _fetchRemoteConfig; this.fetchRemoteConfig = _fetchRemoteConfig; + this.topLevelDomain = topLevelDomain; } get cookieStorage() { @@ -293,9 +295,15 @@ export const useBrowserConfig = async ( ): Promise => { // Step 1: Create identity storage instance const identityStorage = options.identityStorage || DEFAULT_IDENTITY_STORAGE; + let topLevelDomain = ''; + + // use the getTopLevelDomain function to find the TLD only if identity storage + // is cookie (because getTopLevelDomain() uses cookies) + if (identityStorage === DEFAULT_IDENTITY_STORAGE) { + topLevelDomain = await getTopLevelDomain(); + } const cookieOptions = { - domain: - identityStorage !== DEFAULT_IDENTITY_STORAGE ? '' : options.cookieOptions?.domain ?? (await getTopLevelDomain()), + domain: options.cookieOptions?.domain ?? topLevelDomain, expiration: 365, sameSite: 'Lax' as const, secure: false, @@ -401,6 +409,7 @@ export const useBrowserConfig = async ( earlyConfig?.diagnosticsSampleRate ?? amplitudeInstance._diagnosticsSampleRate, diagnosticsClient, options.remoteConfig, + topLevelDomain, ); if (!(await browserConfig.storageProvider.isEnabled())) { diff --git a/packages/analytics-browser/src/types.ts b/packages/analytics-browser/src/types.ts index 2bcec7730..cfe3275c4 100644 --- a/packages/analytics-browser/src/types.ts +++ b/packages/analytics-browser/src/types.ts @@ -41,4 +41,6 @@ export { ReferrerParameters, UTMParameters, ValidPropertyType, + ExcludeInternalReferrersOptions, + EXCLUDE_INTERNAL_REFERRERS_CONDITIONS, } from '@amplitude/analytics-core'; diff --git a/packages/analytics-browser/test/attribution/helpers.test.ts b/packages/analytics-browser/test/attribution/helpers.test.ts index 492379fe4..a819d48bb 100644 --- a/packages/analytics-browser/test/attribution/helpers.test.ts +++ b/packages/analytics-browser/test/attribution/helpers.test.ts @@ -1,12 +1,13 @@ +import { ExcludeInternalReferrersOptions, getStorageKey, BASE_CAMPAIGN } from '@amplitude/analytics-core'; import { isNewCampaign, createCampaignEvent, getDefaultExcludedReferrers, isExcludedReferrer, + isSubdomainOf, + getDomain, } from '../../src/attribution/helpers'; -import { getStorageKey, BASE_CAMPAIGN } from '@amplitude/analytics-core'; - const loggerProvider = { log: jest.fn(), debug: jest.fn(), @@ -173,6 +174,216 @@ describe('isNewCampaign', () => { expect(isNewCampaign(currentCampaign, previousCampaign, {}, loggerProvider, false)).toBe(true); }); + + describe('when excludeInternalReferrers', () => { + let location: Location; + + beforeAll(() => { + location = window.location; + Object.defineProperty(window, 'location', { + value: { + hostname: 'a.b.co.uk', + }, + writable: true, + }); + }); + + afterAll(() => { + Object.defineProperty(window, 'location', { + value: location, + writable: true, + }); + }); + + describe('is true (or "always")', () => { + test('should return false if internal referrer', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'a.b.co.uk', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'b.co.uk', + }; + + expect( + isNewCampaign(currentCampaign, previousCampaign, { excludeInternalReferrers: true }, loggerProvider), + ).toBe(false); + expect(isNewCampaign(currentCampaign, previousCampaign, { excludeInternalReferrers: {} }, loggerProvider)).toBe( + false, + ); + }); + + describe('when cookieDomain is specified', () => { + test('should return false if internal referrer', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'a.b.co.uk', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'b.co.uk', + }; + expect( + isNewCampaign( + currentCampaign, + previousCampaign, + { excludeInternalReferrers: true }, + loggerProvider, + false, + '.b.co.uk', + ), + ).toBe(false); + }); + + test('should return true if not internal referrer', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'www.google.com', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'www.google.co.jp', + }; + expect( + isNewCampaign( + currentCampaign, + previousCampaign, + { excludeInternalReferrers: true }, + loggerProvider, + false, + '.b.co.uk', + ), + ).toBe(true); + }); + }); + + test('should return true if not internal referrer', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'facebook.com', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'google.com', + }; + expect( + isNewCampaign(currentCampaign, previousCampaign, { excludeInternalReferrers: true }, loggerProvider), + ).toBe(true); + }); + + test('should return false if no referring_domain', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + }; + expect( + isNewCampaign(currentCampaign, previousCampaign, { excludeInternalReferrers: true }, loggerProvider), + ).toBe(false); + }); + + test('should return false if no referring domain', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + }; + expect( + isNewCampaign( + currentCampaign, + previousCampaign, + { excludeInternalReferrers: { condition: 'always' } }, + loggerProvider, + ), + ).toBe(false); + }); + }); + + describe('is "ifEmptyCampaign"', () => { + test('should return false if internal referrer and campaign is empty', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'a.b.co.uk', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + referring_domain: 'a.b.co.uk', + }; + expect( + isNewCampaign( + currentCampaign, + previousCampaign, + { excludeInternalReferrers: { condition: 'ifEmptyCampaign' } }, + loggerProvider, + ), + ).toBe(false); + }); + + test('should return true if not internal referrer and campaign is not empty', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + utm_campaign: 'previous_campaign', + referring_domain: 'facebook.com', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + utm_campaign: 'new_campaign', + referring_domain: 'google.com', + }; + expect( + isNewCampaign( + currentCampaign, + previousCampaign, + { excludeInternalReferrers: { condition: 'ifEmptyCampaign' } }, + loggerProvider, + ), + ).toBe(true); + }); + + test('should return true if internal referrer and campaign is not empty', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + utm_campaign: 'previous_campaign', + referring_domain: 'a.b.co.uk', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + utm_campaign: 'new_campaign', + referring_domain: 'a.b.co.uk', + }; + expect( + isNewCampaign( + currentCampaign, + previousCampaign, + { excludeInternalReferrers: { condition: 'ifEmptyCampaign' } }, + loggerProvider, + ), + ).toBe(true); + }); + }); + + describe('is invalid', () => { + test('should silently ignore invalid condition', () => { + const previousCampaign = { + ...BASE_CAMPAIGN, + utm_campaign: 'previous_campaign', + referring_domain: 'a.b.co.uk', + }; + const currentCampaign = { + ...BASE_CAMPAIGN, + utm_campaign: 'new_campaign', + referring_domain: 'a.b.co.uk', + }; + const excludeInternalReferrers = { condition: 'invalid' } as unknown as ExcludeInternalReferrersOptions; + expect(isNewCampaign(currentCampaign, previousCampaign, { excludeInternalReferrers }, loggerProvider)).toBe( + true, + ); + }); + }); + }); }); describe('isExcludedReferrer', () => { @@ -325,3 +536,27 @@ describe('getDefaultExcludedReferrers', () => { expect(excludedReferrers).toEqual([new RegExp('amplitude\\.com$')]); }); }); + +describe('isSubdomainOf', () => { + test('should return true if subdomain of domain', () => { + expect(isSubdomainOf('b.co.uk', 'b.co.uk')).toBe(true); // exact match + expect(isSubdomainOf('b.co.uk', '.b.co.uk')).toBe(true); // exact match leading dot + expect(isSubdomainOf('a.b.co.uk', '.b.co.uk')).toBe(true); + expect(isSubdomainOf('www.b.co.uk', '.b.co.uk')).toBe(true); + expect(isSubdomainOf('www.b.co.uk', '.co.uk')).toBe(true); + expect(isSubdomainOf('www.b.co.uk', 'co.uk')).toBe(true); + expect(isSubdomainOf('.www.b.co.uk', 'b.co.uk')).toBe(true); + }); + + test('should return false if not subdomain of domain', () => { + expect(isSubdomainOf('b.co.uk', 'a.b.co.uk')).toBe(false); + expect(isSubdomainOf('b.co.uk', '.a.b.co.uk')).toBe(false); + expect(isSubdomainOf('www.b.co.uk', 'google.com')).toBe(false); + }); +}); + +describe('getDomain', () => { + test('should return true if both localhost', () => { + expect(getDomain('localhost')).toBe('localhost'); + }); +}); diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index 04ea13efd..7dad8a25e 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -154,6 +154,7 @@ describe('config', () => { remoteConfig: { fetchRemoteConfig: true, }, + topLevelDomain: '.amplitude.com', }); expect(getTopLevelDomain).toHaveBeenCalledTimes(1); }); @@ -262,6 +263,7 @@ describe('config', () => { remoteConfig: { fetchRemoteConfig: true, }, + topLevelDomain: '', }); }); diff --git a/packages/analytics-browser/test/cookie-migration/index.test.ts b/packages/analytics-browser/test/cookie-migration/index.test.ts index 324cd226f..c1e40e016 100644 --- a/packages/analytics-browser/test/cookie-migration/index.test.ts +++ b/packages/analytics-browser/test/cookie-migration/index.test.ts @@ -1,7 +1,6 @@ -import { Storage, UserSession } from '@amplitude/analytics-core'; +import { Storage, UserSession, MemoryStorage, CookieStorage, getOldCookieName } from '@amplitude/analytics-core'; import { decode, parseLegacyCookies, parseTime } from '../../src/cookie-migration'; import * as LocalStorageModule from '../../src/storage/local-storage'; -import { MemoryStorage, CookieStorage, getOldCookieName } from '@amplitude/analytics-core'; describe('cookie-migration', () => { const API_KEY = 'asdfasdf'; diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index bcb6f467d..45776dd9a 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -156,3 +156,5 @@ export { AMPLITUDE_ORIGINS_MAP, AMPLITUDE_BACKGROUND_CAPTURE_SCRIPT_URL, } from './messenger/constants'; + +export { ExcludeInternalReferrersOptions, EXCLUDE_INTERNAL_REFERRERS_CONDITIONS } from './types/config/browser-config'; diff --git a/packages/analytics-core/src/types/config/browser-config.ts b/packages/analytics-core/src/types/config/browser-config.ts index e0108e65f..d440b742f 100644 --- a/packages/analytics-core/src/types/config/browser-config.ts +++ b/packages/analytics-core/src/types/config/browser-config.ts @@ -115,6 +115,7 @@ interface InternalBrowserConfig { diagnosticsClient?: IDiagnosticsClient; remoteConfigClient?: IRemoteConfigClient; deferredSessionId?: number; + topLevelDomain?: string; } /** @@ -226,6 +227,13 @@ export interface AttributionOptions { * @defaultValue `[/your-domain\.com$/]` */ excludeReferrers?: (string | RegExp)[]; + /** + * Exclude internal referrers from campaign attribution. + * (a referrer is 'internal' if it is on the same domain as the current page) + * @experimental this feature is experimental and may not be stable + * @defaultValue `false` + */ + excludeInternalReferrers?: true | false | ExcludeInternalReferrersOptions; /** * The value to represent undefined/no initial campaign parameter for first-touch attribution. * @defaultValue `"EMPTY"` @@ -238,6 +246,22 @@ export interface AttributionOptions { resetSessionOnNewCampaign?: boolean; } +export const EXCLUDE_INTERNAL_REFERRERS_CONDITIONS = { + always: 'always', + ifEmptyCampaign: 'ifEmptyCampaign', +} as const; + +type ExcludeInternalReferrersCondition = + (typeof EXCLUDE_INTERNAL_REFERRERS_CONDITIONS)[keyof typeof EXCLUDE_INTERNAL_REFERRERS_CONDITIONS]; + +export interface ExcludeInternalReferrersOptions { + /* + * The condition on which to exclude internal referrers for campaign attribution. + * @defaultValue `"always"` + */ + condition?: ExcludeInternalReferrersCondition; +} + export interface RemoteConfigOptions { /** * Whether to fetch remote configuration. The remote configuration can be updated in the Amplitude platform here: diff --git a/packages/analytics-core/test/index.test.ts b/packages/analytics-core/test/index.test.ts index a68512699..fac723485 100644 --- a/packages/analytics-core/test/index.test.ts +++ b/packages/analytics-core/test/index.test.ts @@ -70,6 +70,7 @@ import { AMPLITUDE_ORIGIN_STAGING, AMPLITUDE_ORIGINS_MAP, AMPLITUDE_BACKGROUND_CAPTURE_SCRIPT_URL, + EXCLUDE_INTERNAL_REFERRERS_CONDITIONS, } from '../src/index'; describe('index', () => { @@ -159,6 +160,18 @@ describe('index', () => { expect(typeof AMPLITUDE_ORIGIN_STAGING).toBe('string'); expect(typeof AMPLITUDE_ORIGINS_MAP).toBe('object'); expect(typeof AMPLITUDE_BACKGROUND_CAPTURE_SCRIPT_URL).toBe('string'); + expect(typeof EXCLUDE_INTERNAL_REFERRERS_CONDITIONS).toBe('object'); + }); + + describe('EXCLUDE_INTERNAL_REFERRERS_CONDITIONS export', () => { + test('should be an object', () => { + expect(typeof EXCLUDE_INTERNAL_REFERRERS_CONDITIONS).toBe('object'); + }); + test('keys and values should be strings with same value', () => { + Object.entries(EXCLUDE_INTERNAL_REFERRERS_CONDITIONS).forEach(([key, value]) => { + expect(key).toBe(value); + }); + }); }); describe('replaceSensitiveString export', () => { diff --git a/test-server/analytics-snippet/index.html b/test-server/analytics-snippet/index.html index 018de9bce..26f88919a 100644 --- a/test-server/analytics-snippet/index.html +++ b/test-server/analytics-snippet/index.html @@ -3,7 +3,7 @@ Analytics Snippet Test diff --git a/test-server/autocapture/element-interactions.html b/test-server/autocapture/element-interactions.html index b910425da..d37a6e398 100644 --- a/test-server/autocapture/element-interactions.html +++ b/test-server/autocapture/element-interactions.html @@ -274,7 +274,18 @@

Content Changing Button

import.meta.env.VITE_AMPLITUDE_USER_ID || 'amplitude-typescript test user', { fetchRemoteConfig: false, + logLevel: 'debug', autocapture: { + attribution: { + excludeReferrers: [ + /\.google.com$/, + ], + excludeInternalReferrers: { + condition: 'ifEmptyCampaign', + //condition: 'always', + }, + //excludeInternalReferrers: false, + }, elementInteractions: { debounceTime: 1000, },