From e264c182a230ce6f200f835cc716de1d25664c52 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 25 Sep 2025 15:12:56 +0100 Subject: [PATCH 01/38] [Amplitude] - Attribution Autocapture --- .../src/sessionId/autocapture-attribution.ts | 47 +++++++++++++++++++ .../src/sessionId/constants.ts | 7 +++ .../src/sessionId/generated-types.ts | 4 ++ .../amplitude-plugins/src/sessionId/index.ts | 18 +++++++ .../amplitude-plugins/src/sessionId/types.ts | 5 ++ 5 files changed, 81 insertions(+) create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/constants.ts create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/types.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts new file mode 100644 index 00000000000..1a612d9c552 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts @@ -0,0 +1,47 @@ + +import { UniversalStorage } from '@segment/analytics-next' +import type { AttributionKey, AttributionValues } from './types' +import { KEYS, ATTRIBUTION_STORAGE_KEY } from './constants' + +export function getAttributionFromURL(queryString: string | undefined): Partial { + if (!queryString){ + return {} + } + + const urlParams = new URLSearchParams(queryString) + + return Object.fromEntries( + KEYS + .map(key => [key, urlParams.get(key)] as const) + .filter(([, value]) => value !== null) + ) as Partial +} + +export function getAttributionFromStorage(storage: UniversalStorage>): Partial { + const values = storage.get(ATTRIBUTION_STORAGE_KEY) + return values ?? {} +} + +export function getAttributionDiff( + oldValues: Partial, + newValues: Partial +): { itemsToSet: Partial, itemsToUnset: AttributionKey[], changes: boolean } { + const itemsToSet: Partial = {} + + KEYS.forEach((key) => { + const newVal = newValues[key] ?? null + if (newVal !== null && newVal !== oldValues[key]) { + itemsToSet[key] = newVal + } + }) + + const itemsToUnset = KEYS.filter( + (key) => oldValues[key] !== null && !(key in itemsToSet) + ) + + return { + itemsToSet, + itemsToUnset, + changes: Object.entries(itemsToSet).length > 0 || Object.entries(itemsToUnset).length > 0 + } +} diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/constants.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/constants.ts new file mode 100644 index 00000000000..75f4e3687e8 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/constants.ts @@ -0,0 +1,7 @@ +export const KEYS = [ + 'utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_id', + 'gclid','wbraid','gbraid', + 'msclkid','fbclid','ko_clickid','li_fat_id','rtd_cid','ttclid','twclid' +] as const + +export const ATTRIBUTION_STORAGE_KEY = 'amplitude-attribution-params' \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts index 8ed67ee8742..7d58e9202f5 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts @@ -5,4 +5,8 @@ export interface Payload { * Time in milliseconds to be used before considering a session stale. */ sessionLength?: number + /** + * Whether to automatically capture latest interaction attribution data from the URL. + */ + autocaptureAttribution: boolean } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts index 24aa9d89273..74e92a69f2f 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts @@ -3,6 +3,8 @@ import { UniversalStorage } from '@segment/analytics-next' import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' +import { getAttributionFromURL, getAttributionFromStorage, getAttributionDiff } from './autocapture-attribution' +import { AttributionValues } from './types' function newSessionId(): number { return now() @@ -40,6 +42,12 @@ const action: BrowserActionDefinition = { type: 'number', required: false, description: 'Time in milliseconds to be used before considering a session stale.' + }, + autocaptureAttribution: { + label: 'Autocapture Attribution', + type: 'boolean', + required: true, + description: 'Whether to automatically capture latest interaction attribution data from the URL.' } }, lifecycleHook: 'enrichment', @@ -81,6 +89,16 @@ const action: BrowserActionDefinition = { context.updateEvent('integrations.Actions Amplitude.session_id', id) } + if (payload.autocaptureAttribution) { + const urlParams = getAttributionFromURL(window.location.search) + const storedParams = getAttributionFromStorage(analytics.storage as UniversalStorage>) + const { itemsToSet, itemsToUnset, changes } = getAttributionDiff(storedParams, urlParams) + if (changes && (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude'])) { + context.updateEvent('integrations.Actions Amplitude', {}) + context.updateEvent('integrations.Actions Amplitude.attribution', { itemsToSet, itemsToUnset }) + } + } + return } } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/types.ts new file mode 100644 index 00000000000..2a629b30084 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/types.ts @@ -0,0 +1,5 @@ +import { KEYS } from './constants' + +export type AttributionKey = typeof KEYS[number] + +export type AttributionValues = Record From 615f231ee64c9f12c8b2aed6030a1ee0f5e633ee Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 26 Sep 2025 13:38:03 +0100 Subject: [PATCH 02/38] saving progress --- .../src/sessionId/autocapture-attribution.ts | 32 +++++++++---------- .../amplitude-plugins/src/sessionId/index.ts | 13 ++++---- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts index 1a612d9c552..9a3d579ccc0 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts @@ -3,7 +3,7 @@ import { UniversalStorage } from '@segment/analytics-next' import type { AttributionKey, AttributionValues } from './types' import { KEYS, ATTRIBUTION_STORAGE_KEY } from './constants' -export function getAttributionFromURL(queryString: string | undefined): Partial { +export function getAttributionsFromURL(queryString: string | undefined): Partial { if (!queryString){ return {} } @@ -17,31 +17,31 @@ export function getAttributionFromURL(queryString: string | undefined): Partial< ) as Partial } -export function getAttributionFromStorage(storage: UniversalStorage>): Partial { +export function getAttributionsFromStorage(storage: UniversalStorage>): Partial { const values = storage.get(ATTRIBUTION_STORAGE_KEY) return values ?? {} } -export function getAttributionDiff( - oldValues: Partial, - newValues: Partial -): { itemsToSet: Partial, itemsToUnset: AttributionKey[], changes: boolean } { - const itemsToSet: Partial = {} +export function getAttributionsDiff( + cachedAttributions: Partial, + urlAttributions: Partial +): { new_attributions: Partial, old_attributions: AttributionKey[], differences: boolean } { + const newAttributions: Partial = {} KEYS.forEach((key) => { - const newVal = newValues[key] ?? null - if (newVal !== null && newVal !== oldValues[key]) { - itemsToSet[key] = newVal + const newVal = urlAttributions[key] ?? null + if (newVal !== null && newVal !== cachedAttributions[key]) { + newAttributions[key] = newVal } }) - const itemsToUnset = KEYS.filter( - (key) => oldValues[key] !== null && !(key in itemsToSet) + const oldAttributions = KEYS.filter( + (key) => cachedAttributions[key] !== null && !(key in newAttributions) ) return { - itemsToSet, - itemsToUnset, - changes: Object.entries(itemsToSet).length > 0 || Object.entries(itemsToUnset).length > 0 + new_attributions: newAttributions, + old_attributions: oldAttributions, + differences: Object.entries(newAttributions).length > 0 || Object.entries(oldAttributions).length > 0 } -} +} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts index 74e92a69f2f..6f0722237c8 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts @@ -3,7 +3,7 @@ import { UniversalStorage } from '@segment/analytics-next' import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { getAttributionFromURL, getAttributionFromStorage, getAttributionDiff } from './autocapture-attribution' +import { getAttributionsFromURL, getAttributionsFromStorage, getAttributionsDiff } from './autocapture-attribution' import { AttributionValues } from './types' function newSessionId(): number { @@ -90,12 +90,11 @@ const action: BrowserActionDefinition = { } if (payload.autocaptureAttribution) { - const urlParams = getAttributionFromURL(window.location.search) - const storedParams = getAttributionFromStorage(analytics.storage as UniversalStorage>) - const { itemsToSet, itemsToUnset, changes } = getAttributionDiff(storedParams, urlParams) - if (changes && (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude'])) { + const urlAttributions = getAttributionsFromURL(window.location.search) + const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>) + if (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude']) { context.updateEvent('integrations.Actions Amplitude', {}) - context.updateEvent('integrations.Actions Amplitude.attribution', { itemsToSet, itemsToUnset }) + context.updateEvent('integrations.Actions Amplitude.autocapture_attribution', getAttributionsDiff(cachedAttributions, urlAttributions)) } } @@ -103,4 +102,4 @@ const action: BrowserActionDefinition = { } } -export default action +export default action \ No newline at end of file From b128c6ffc47ce7c4ff1bc4f42fa7b289cc4e108c Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 3 Nov 2025 13:02:15 +0000 Subject: [PATCH 03/38] splitting to separate actions --- .../__tests__/sessionId.test.ts | 358 ++++++++++++++++++ .../constants.ts | 0 .../functions.ts} | 0 .../autocaptureAttribution/generated-types.ts | 8 + .../src/autocaptureAttribution/index.ts | 36 ++ .../types.ts | 0 .../src/sessionId/generated-types.ts | 4 - .../amplitude-plugins/src/sessionId/index.ts | 17 - 8 files changed, 402 insertions(+), 21 deletions(-) create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts rename packages/browser-destinations/destinations/amplitude-plugins/src/{sessionId => autocaptureAttribution}/constants.ts (100%) rename packages/browser-destinations/destinations/amplitude-plugins/src/{sessionId/autocapture-attribution.ts => autocaptureAttribution/functions.ts} (100%) create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts rename packages/browser-destinations/destinations/amplitude-plugins/src/{sessionId => autocaptureAttribution}/types.ts (100%) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts new file mode 100644 index 00000000000..dfd274d3ec2 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts @@ -0,0 +1,358 @@ +import { Analytics, Context, Plugin } from '@segment/analytics-next' +import browserPluginsDestination from '../..' +import { Subscription } from '@segment/browser-destination-runtime/types' +import jar from 'js-cookie' + +expect.extend({ + toBeWithinOneSecondOf(got, expected) { + if (typeof got === 'string') { + got = parseInt(got, 10) + } + + if (typeof expected === 'string') { + expected = parseInt(expected, 10) + } + + const oneSecond = 1000 + + const timeDiff = Math.abs(expected - got) + const timeDiffInSeconds = timeDiff / 1000 + + const pass = timeDiff < oneSecond + const message = () => + `${got} should be within a second of ${expected}, ` + `actual difference: ${timeDiffInSeconds.toFixed(1)}s` + + return { pass, message } + } +}) + +const example: Subscription[] = [ + { + partnerAction: 'sessionId', + name: 'SessionId', + enabled: true, + subscribe: 'type = "track"', + mapping: {} + } +] + +let browserActions: Plugin[] +let sessionIdPlugin: Plugin +let ajs: Analytics + +beforeEach(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + sessionIdPlugin = browserActions[0] + + // clear storage and cookies + document.cookie.split(';').forEach(function (c) { + document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/') + }) + window.localStorage.clear() + + ajs = new Analytics({ + writeKey: 'w_123' + }) +}) + +describe('ajs-integration', () => { + test('updates the original event with a session id', async () => { + await sessionIdPlugin.load(Context.system(), ajs) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).not.toBeUndefined() + // @ts-expect-error + expect(typeof updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBe('number') + }) + + test('updates the original eveent when All: false but Actions Amplitude: true', async () => { + await sessionIdPlugin.load(Context.system(), ajs) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + }, + integrations: { + All: false, + 'Actions Amplitude': true + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).not.toBeUndefined() + // @ts-expect-error + expect(typeof updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBe('number') + }) + + test('doesnt update the original event with a session id when All: false', async () => { + await sessionIdPlugin.load(Context.system(), ajs) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + }, + integrations: { + All: false + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeUndefined() + }) + + test('runs as an enrichment middleware', async () => { + await ajs.register(sessionIdPlugin) + jest.spyOn(sessionIdPlugin, 'track') + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + await ajs.track(ctx.event) + + expect(sessionIdPlugin.track).toHaveBeenCalled() + expect(ajs.queue.plugins.map((p) => ({ name: p.name, type: p.type }))).toMatchInlineSnapshot(` + Array [ + Object { + "name": "Amplitude (Actions) sessionId", + "type": "enrichment", + }, + ] + `) + }) +}) + +describe('sessionId', () => { + beforeEach(async () => { + jest.useFakeTimers('legacy') + await sessionIdPlugin.load(Context.system(), ajs) + }) + + const id = () => new Date().getTime() + + describe('new sessions', () => { + test('sets a session id', async () => { + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(id()) + }) + + test('persists the session id', async () => { + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + await sessionIdPlugin.track?.(ctx) + + // persists the session id in both cookies and local storage + expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(id().toString()) + expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(id().toString()) + expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(id().toString()) + expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(id().toString()) + expect(jar.get('analytics_session_id')).toBe(window.localStorage.getItem('analytics_session_id')) + expect(jar.get('analytics_session_id.last_access')).toBe(window.localStorage.getItem('analytics_session_id')) + }) + }) + + describe('existing sessions', () => { + test('uses an existing session id in LS', async () => { + const then = id() + jest.advanceTimersByTime(10000) + + window.localStorage.setItem('analytics_session_id', then.toString()) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) + }) + + test('sync session info from LS to cookies', async () => { + const then = id() + + window.localStorage.setItem('analytics_session_id', then.toString()) + window.localStorage.setItem('analytics_session_id.last_access', then.toString()) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) + expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(then) + }) + + test('uses an existing session id stored in cookies and sync it with local storage', async () => { + const then = id() + jest.advanceTimersByTime(10000) + jar.set('analytics_session_id', then.toString()) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + const now = id() + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) + // synced to local storage + expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now) + expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(then) + }) + + test('keeps track of when the session was last accessed', async () => { + const then = id() + jest.advanceTimersByTime(10000) + window.localStorage.setItem('analytics_session_id', then.toString()) + + const now = id() + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) + + expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now) + expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(now) + }) + + test('reset session when stale', async () => { + const then = id() + window.localStorage.setItem('analytics_session_id.last_access', then.toString()) + window.localStorage.setItem('analytics_session_id', then.toString()) + + const THIRTY_MINUTES = 30 * 60000 + jest.advanceTimersByTime(THIRTY_MINUTES) + + const now = id() + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(now) + + expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(now.toString()) + expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString()) + expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(now.toString()) + expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString()) + }) + }) + + describe('work without AJS storage layer', () => { + test('uses an existing session id in LS when AJS storage layer is not available', async () => { + const then = id() + //@ts-expect-error + jest.spyOn(ajs, 'storage', 'get').mockReturnValue(undefined) + jest.advanceTimersByTime(10000) + + window.localStorage.setItem('analytics_session_id', then.toString()) + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) + expect(jar.get('analytics_session_id')).toBe(undefined) + }) + + test('uses an existing session id in LS when AJS storage layer is not available', async () => { + const then = id() + //@ts-expect-error + jest.spyOn(ajs, 'storage', 'get').mockReturnValue(undefined) + + window.localStorage.setItem('analytics_session_id.last_access', then.toString()) + window.localStorage.setItem('analytics_session_id', then.toString()) + + const THIRTY_MINUTES = 30 * 60000 + jest.advanceTimersByTime(THIRTY_MINUTES) + + const now = id() + + const ctx = new Context({ + type: 'track', + event: 'greet', + properties: { + greeting: 'Oi!' + } + }) + + const updatedCtx = await sessionIdPlugin.track?.(ctx) + // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` + expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(now) + + expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(now.toString()) + expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString()) + expect(jar.get('analytics_session_id')).toBeUndefined() + expect(jar.get('analytics_session_id.last_access')).toBeUndefined() + }) + }) +}) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/constants.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts similarity index 100% rename from packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/constants.ts rename to packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts similarity index 100% rename from packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution.ts rename to packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts new file mode 100644 index 00000000000..b7aa525dca1 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Whether to automatically capture latest interaction attribution data from the URL. + */ + autocaptureAttribution: boolean +} diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts new file mode 100644 index 00000000000..4e2ee7d84d5 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { UniversalStorage } from '@segment/analytics-next' +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { getAttributionsFromURL, getAttributionsFromStorage, getAttributionsDiff } from './functions' +import { AttributionValues } from './types' + +const action: BrowserActionDefinition = { + title: 'Autocapture Attribution Plugin', + description: 'Captures attribution details from the URL and attaches it to every Amplitude browser based event.', + platform: 'web', + hidden: false, + defaultSubscription: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', + fields: { + autocaptureAttribution: { + label: 'Autocapture Attribution', + type: 'boolean', + required: true, + description: 'Whether to automatically capture latest interaction attribution data from the URL.' + } + }, + lifecycleHook: 'enrichment', + perform: (_, { context, payload, analytics }) => { + if (payload.autocaptureAttribution) { + const urlAttributions = getAttributionsFromURL(window.location.search) + const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>) + if (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude']) { + context.updateEvent('integrations.Actions Amplitude', {}) + context.updateEvent('integrations.Actions Amplitude.autocapture_attribution', getAttributionsDiff(cachedAttributions, urlAttributions)) + } + } + return + } +} +export default action \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/types.ts similarity index 100% rename from packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/types.ts rename to packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/types.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts index 7d58e9202f5..8ed67ee8742 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts @@ -5,8 +5,4 @@ export interface Payload { * Time in milliseconds to be used before considering a session stale. */ sessionLength?: number - /** - * Whether to automatically capture latest interaction attribution data from the URL. - */ - autocaptureAttribution: boolean } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts index 6f0722237c8..59e1b7e42eb 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts @@ -3,8 +3,6 @@ import { UniversalStorage } from '@segment/analytics-next' import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { getAttributionsFromURL, getAttributionsFromStorage, getAttributionsDiff } from './autocapture-attribution' -import { AttributionValues } from './types' function newSessionId(): number { return now() @@ -42,12 +40,6 @@ const action: BrowserActionDefinition = { type: 'number', required: false, description: 'Time in milliseconds to be used before considering a session stale.' - }, - autocaptureAttribution: { - label: 'Autocapture Attribution', - type: 'boolean', - required: true, - description: 'Whether to automatically capture latest interaction attribution data from the URL.' } }, lifecycleHook: 'enrichment', @@ -89,15 +81,6 @@ const action: BrowserActionDefinition = { context.updateEvent('integrations.Actions Amplitude.session_id', id) } - if (payload.autocaptureAttribution) { - const urlAttributions = getAttributionsFromURL(window.location.search) - const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>) - if (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude']) { - context.updateEvent('integrations.Actions Amplitude', {}) - context.updateEvent('integrations.Actions Amplitude.autocapture_attribution', getAttributionsDiff(cachedAttributions, urlAttributions)) - } - } - return } } From d8ff9a52e7dc9d9acbf798eef1d79582a09c8e89 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 3 Nov 2025 14:53:40 +0000 Subject: [PATCH 04/38] Unit tests added --- .../__tests__/index.test.ts | 182 +++++++++ .../__tests__/sessionId.test.ts | 358 ------------------ .../src/autocaptureAttribution/constants.ts | 20 +- .../src/autocaptureAttribution/functions.ts | 30 +- .../autocaptureAttribution/generated-types.ts | 7 +- .../src/autocaptureAttribution/index.ts | 34 +- .../amplitude-plugins/src/constants.ts | 1 + .../amplitude-plugins/src/index.ts | 4 +- .../amplitude-plugins/src/sessionId/index.ts | 7 +- 9 files changed, 228 insertions(+), 415 deletions(-) create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts delete mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/constants.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts new file mode 100644 index 00000000000..5cec727d1af --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -0,0 +1,182 @@ +import { Analytics, Context, Plugin } from '@segment/analytics-next' +import { Subscription } from '@segment/browser-destination-runtime/types' +import browserPluginsDestination from '../../' +import { DESTINATION_INTEGRATION_NAME } from '../../constants' + +const example: Subscription[] = [ + { + partnerAction: 'autocaptureAttribution', + name: 'Autocapture Attribution Plugin', + enabled: true, + subscribe: 'type = "track"', + mapping: {} + } +] + +let browserActions: Plugin[] +let autocaptureAttributionPlugin: Plugin +let ajs: Analytics + +beforeAll(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + autocaptureAttributionPlugin = browserActions[0] + + ajs = new Analytics({ + writeKey: 'w_123' + }) + + // window.localStorage.clear() + + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale&utm_term=running+shoes&utm_content=ad1&gclid=gclid1234&gbraid=gbraid5678' + }, + writable: true + }) +}) + +describe('ajs-integration', () => { + test('updates the original event with with attributions values from the URL, caches the values, then updates when new values come along', async () => { + await autocaptureAttributionPlugin.load(Context.system(), ajs) + const ctx = new Context({ + type: 'track', + event: 'Test Event', + properties: { + greeting: 'Yo!' + } + }) + + + /* + * First we test that the attributions from the URL are captured and added to the event + */ + const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj).toEqual( + { + autocapture_attribution: { + new: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + }, + old: {} + } + } + ) + + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=email' + }, + writable: true + }) + + /* + * Then we test that the new attributes from the URL are captured, the old cached values are retrieved + */ + const updatedCtx2 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj2 = updatedCtx2?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj2).toEqual( + { + autocapture_attribution: { + new: { + utm_source: "email" + }, + old: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + } + } + } + ) + + + Object.defineProperty(window, 'location', { + value: { + search: '?' + }, + writable: true + }) + + + /* + * Then we test when there are no new URL attribution values - the last cached attribution values are passed correctly in the payload + */ + const updatedCtx3 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj3 = updatedCtx3?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj3).toEqual( + { + autocapture_attribution: { + new: {}, + old: { + utm_source: "email" + } + } + } + ) + + + Object.defineProperty(window, 'location', { + value: { + search: '?some_fake_non_attribution_param=12345' + }, + writable: true + }) + + /* + * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload + */ + const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj4).toEqual( + { + autocapture_attribution: { + new: {}, + old: { + utm_source: "email" + } + } + } + ) + + Object.defineProperty(window, 'location', { + value: { + search: '?ttclid=uyiuyiuy' + }, + writable: true + }) + + /* + * Finally we test with a completely new attribution parameter + */ + const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj5).toEqual( + { + autocapture_attribution: { + new: { + ttclid: "uyiuyiuy" + }, + old: { + utm_source: "email" + } + } + } + ) + + }) +}) \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts deleted file mode 100644 index dfd274d3ec2..00000000000 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/sessionId.test.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { Analytics, Context, Plugin } from '@segment/analytics-next' -import browserPluginsDestination from '../..' -import { Subscription } from '@segment/browser-destination-runtime/types' -import jar from 'js-cookie' - -expect.extend({ - toBeWithinOneSecondOf(got, expected) { - if (typeof got === 'string') { - got = parseInt(got, 10) - } - - if (typeof expected === 'string') { - expected = parseInt(expected, 10) - } - - const oneSecond = 1000 - - const timeDiff = Math.abs(expected - got) - const timeDiffInSeconds = timeDiff / 1000 - - const pass = timeDiff < oneSecond - const message = () => - `${got} should be within a second of ${expected}, ` + `actual difference: ${timeDiffInSeconds.toFixed(1)}s` - - return { pass, message } - } -}) - -const example: Subscription[] = [ - { - partnerAction: 'sessionId', - name: 'SessionId', - enabled: true, - subscribe: 'type = "track"', - mapping: {} - } -] - -let browserActions: Plugin[] -let sessionIdPlugin: Plugin -let ajs: Analytics - -beforeEach(async () => { - browserActions = await browserPluginsDestination({ subscriptions: example }) - sessionIdPlugin = browserActions[0] - - // clear storage and cookies - document.cookie.split(';').forEach(function (c) { - document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/') - }) - window.localStorage.clear() - - ajs = new Analytics({ - writeKey: 'w_123' - }) -}) - -describe('ajs-integration', () => { - test('updates the original event with a session id', async () => { - await sessionIdPlugin.load(Context.system(), ajs) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).not.toBeUndefined() - // @ts-expect-error - expect(typeof updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBe('number') - }) - - test('updates the original eveent when All: false but Actions Amplitude: true', async () => { - await sessionIdPlugin.load(Context.system(), ajs) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - }, - integrations: { - All: false, - 'Actions Amplitude': true - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).not.toBeUndefined() - // @ts-expect-error - expect(typeof updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBe('number') - }) - - test('doesnt update the original event with a session id when All: false', async () => { - await sessionIdPlugin.load(Context.system(), ajs) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - }, - integrations: { - All: false - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeUndefined() - }) - - test('runs as an enrichment middleware', async () => { - await ajs.register(sessionIdPlugin) - jest.spyOn(sessionIdPlugin, 'track') - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - await ajs.track(ctx.event) - - expect(sessionIdPlugin.track).toHaveBeenCalled() - expect(ajs.queue.plugins.map((p) => ({ name: p.name, type: p.type }))).toMatchInlineSnapshot(` - Array [ - Object { - "name": "Amplitude (Actions) sessionId", - "type": "enrichment", - }, - ] - `) - }) -}) - -describe('sessionId', () => { - beforeEach(async () => { - jest.useFakeTimers('legacy') - await sessionIdPlugin.load(Context.system(), ajs) - }) - - const id = () => new Date().getTime() - - describe('new sessions', () => { - test('sets a session id', async () => { - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(id()) - }) - - test('persists the session id', async () => { - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - await sessionIdPlugin.track?.(ctx) - - // persists the session id in both cookies and local storage - expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(id().toString()) - expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(id().toString()) - expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(id().toString()) - expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(id().toString()) - expect(jar.get('analytics_session_id')).toBe(window.localStorage.getItem('analytics_session_id')) - expect(jar.get('analytics_session_id.last_access')).toBe(window.localStorage.getItem('analytics_session_id')) - }) - }) - - describe('existing sessions', () => { - test('uses an existing session id in LS', async () => { - const then = id() - jest.advanceTimersByTime(10000) - - window.localStorage.setItem('analytics_session_id', then.toString()) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) - }) - - test('sync session info from LS to cookies', async () => { - const then = id() - - window.localStorage.setItem('analytics_session_id', then.toString()) - window.localStorage.setItem('analytics_session_id.last_access', then.toString()) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) - expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(then) - }) - - test('uses an existing session id stored in cookies and sync it with local storage', async () => { - const then = id() - jest.advanceTimersByTime(10000) - jar.set('analytics_session_id', then.toString()) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - const now = id() - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) - // synced to local storage - expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now) - expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(then) - }) - - test('keeps track of when the session was last accessed', async () => { - const then = id() - jest.advanceTimersByTime(10000) - window.localStorage.setItem('analytics_session_id', then.toString()) - - const now = id() - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) - - expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now) - expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(now) - }) - - test('reset session when stale', async () => { - const then = id() - window.localStorage.setItem('analytics_session_id.last_access', then.toString()) - window.localStorage.setItem('analytics_session_id', then.toString()) - - const THIRTY_MINUTES = 30 * 60000 - jest.advanceTimersByTime(THIRTY_MINUTES) - - const now = id() - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(now) - - expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(now.toString()) - expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString()) - expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(now.toString()) - expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString()) - }) - }) - - describe('work without AJS storage layer', () => { - test('uses an existing session id in LS when AJS storage layer is not available', async () => { - const then = id() - //@ts-expect-error - jest.spyOn(ajs, 'storage', 'get').mockReturnValue(undefined) - jest.advanceTimersByTime(10000) - - window.localStorage.setItem('analytics_session_id', then.toString()) - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then) - expect(jar.get('analytics_session_id')).toBe(undefined) - }) - - test('uses an existing session id in LS when AJS storage layer is not available', async () => { - const then = id() - //@ts-expect-error - jest.spyOn(ajs, 'storage', 'get').mockReturnValue(undefined) - - window.localStorage.setItem('analytics_session_id.last_access', then.toString()) - window.localStorage.setItem('analytics_session_id', then.toString()) - - const THIRTY_MINUTES = 30 * 60000 - jest.advanceTimersByTime(THIRTY_MINUTES) - - const now = id() - - const ctx = new Context({ - type: 'track', - event: 'greet', - properties: { - greeting: 'Oi!' - } - }) - - const updatedCtx = await sessionIdPlugin.track?.(ctx) - // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations` - expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(now) - - expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(now.toString()) - expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString()) - expect(jar.get('analytics_session_id')).toBeUndefined() - expect(jar.get('analytics_session_id.last_access')).toBeUndefined() - }) - }) -}) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts index 75f4e3687e8..9d01e230cdd 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts @@ -1,7 +1,21 @@ export const KEYS = [ - 'utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_id', - 'gclid','wbraid','gbraid', - 'msclkid','fbclid','ko_clickid','li_fat_id','rtd_cid','ttclid','twclid' + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'utm_id', + 'dclid', + 'fbclid', + 'gbraid', + 'wbraid', + 'gclid', + 'ko_clickid', + 'li_fat_id', + 'msclkid', + 'rtd_cid', + 'ttclid', + 'twclid' ] as const export const ATTRIBUTION_STORAGE_KEY = 'amplitude-attribution-params' \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts index 9a3d579ccc0..e15149a1268 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts @@ -1,9 +1,9 @@ import { UniversalStorage } from '@segment/analytics-next' -import type { AttributionKey, AttributionValues } from './types' +import type { AttributionValues } from './types' import { KEYS, ATTRIBUTION_STORAGE_KEY } from './constants' -export function getAttributionsFromURL(queryString: string | undefined): Partial { +export function getAttributionsFromURL(queryString: string): Partial { if (!queryString){ return {} } @@ -17,31 +17,11 @@ export function getAttributionsFromURL(queryString: string | undefined): Partial ) as Partial } -export function getAttributionsFromStorage(storage: UniversalStorage>): Partial { +export function getAttributionsFromStorage(storage: UniversalStorage>>): Partial { const values = storage.get(ATTRIBUTION_STORAGE_KEY) return values ?? {} } -export function getAttributionsDiff( - cachedAttributions: Partial, - urlAttributions: Partial -): { new_attributions: Partial, old_attributions: AttributionKey[], differences: boolean } { - const newAttributions: Partial = {} - - KEYS.forEach((key) => { - const newVal = urlAttributions[key] ?? null - if (newVal !== null && newVal !== cachedAttributions[key]) { - newAttributions[key] = newVal - } - }) - - const oldAttributions = KEYS.filter( - (key) => cachedAttributions[key] !== null && !(key in newAttributions) - ) - - return { - new_attributions: newAttributions, - old_attributions: oldAttributions, - differences: Object.entries(newAttributions).length > 0 || Object.entries(oldAttributions).length > 0 - } +export function setAttributionsInStorage(storage: UniversalStorage>>, attributions: Partial): void { + storage.set(ATTRIBUTION_STORAGE_KEY, attributions) } \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts index b7aa525dca1..74523ad652e 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts @@ -1,8 +1,3 @@ // Generated file. DO NOT MODIFY IT BY HAND. -export interface Payload { - /** - * Whether to automatically capture latest interaction attribution data from the URL. - */ - autocaptureAttribution: boolean -} +export interface Payload {} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index 4e2ee7d84d5..c0a147ab3fc 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -3,32 +3,28 @@ import { UniversalStorage } from '@segment/analytics-next' import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { getAttributionsFromURL, getAttributionsFromStorage, getAttributionsDiff } from './functions' +import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' import { AttributionValues } from './types' +import { DESTINATION_INTEGRATION_NAME } from '../constants' const action: BrowserActionDefinition = { title: 'Autocapture Attribution Plugin', - description: 'Captures attribution details from the URL and attaches it to every Amplitude browser based event.', + description: 'Captures attribution details from the URL and attaches them to every Amplitude browser based event.', platform: 'web', - hidden: false, defaultSubscription: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', - fields: { - autocaptureAttribution: { - label: 'Autocapture Attribution', - type: 'boolean', - required: true, - description: 'Whether to automatically capture latest interaction attribution data from the URL.' - } - }, + fields: {}, lifecycleHook: 'enrichment', - perform: (_, { context, payload, analytics }) => { - if (payload.autocaptureAttribution) { - const urlAttributions = getAttributionsFromURL(window.location.search) - const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>) - if (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude']) { - context.updateEvent('integrations.Actions Amplitude', {}) - context.updateEvent('integrations.Actions Amplitude.autocapture_attribution', getAttributionsDiff(cachedAttributions, urlAttributions)) - } + perform: (_, { context, analytics }) => { + const urlAttributions = getAttributionsFromURL(window.location.search) + const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) + + if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { old: cachedAttributions, new: urlAttributions }) + } + + if(Object.entries(urlAttributions).length >0) { + setAttributionsInStorage(analytics.storage as UniversalStorage>>, urlAttributions) } return } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/constants.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/constants.ts new file mode 100644 index 00000000000..a1a2643db79 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/constants.ts @@ -0,0 +1 @@ +export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts index 9227d87747b..9a19c414f9d 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts @@ -2,12 +2,14 @@ import type { Settings } from './generated-types' import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types' import { browserDestination } from '@segment/browser-destination-runtime/shim' import sessionId from './sessionId' +import autocaptureAttribution from './autocaptureAttribution' export const destination: BrowserDestinationDefinition = { name: 'Amplitude (Actions)', mode: 'device', actions: { - sessionId + sessionId, + autocaptureAttribution }, initialize: async () => { return {} diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts index 59e1b7e42eb..23c33919ac6 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts @@ -3,6 +3,7 @@ import { UniversalStorage } from '@segment/analytics-next' import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' +import { DESTINATION_INTEGRATION_NAME } from '../constants' function newSessionId(): number { return now() @@ -76,9 +77,9 @@ const action: BrowserActionDefinition = { storage.set('analytics_session_id.last_access', newSession) - if (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude']) { - context.updateEvent('integrations.Actions Amplitude', {}) - context.updateEvent('integrations.Actions Amplitude.session_id', id) + if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.session_id`, id) } return From e5fd8827164718bf0bd1238744d0aa0a43e0be0e Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 3 Nov 2025 15:09:19 +0000 Subject: [PATCH 05/38] updating tests --- .../__tests__/index.test.ts | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index 5cec727d1af..73c8a0639f4 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -46,7 +46,6 @@ describe('ajs-integration', () => { } }) - /* * First we test that the attributions from the URL are captured and added to the event */ @@ -69,6 +68,40 @@ describe('ajs-integration', () => { } ) + /* + * Then we test the what happens when exact same attributions from the URL are captured and added to the next event + */ + const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj1).toEqual( + { + autocapture_attribution: { + new: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + }, + old: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + } + } + } + ) + + + /* + * Then we test that the new attributes from the URL are captured, the old cached values are retrieved + */ Object.defineProperty(window, 'location', { value: { search: '?utm_source=email' @@ -76,9 +109,6 @@ describe('ajs-integration', () => { writable: true }) - /* - * Then we test that the new attributes from the URL are captured, the old cached values are retrieved - */ const updatedCtx2 = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj2 = updatedCtx2?.event?.integrations[DESTINATION_INTEGRATION_NAME] @@ -101,7 +131,9 @@ describe('ajs-integration', () => { } ) - + /* + * Then we test when there are no new URL attribution values - the last cached attribution values are passed correctly in the payload + */ Object.defineProperty(window, 'location', { value: { search: '?' @@ -109,10 +141,6 @@ describe('ajs-integration', () => { writable: true }) - - /* - * Then we test when there are no new URL attribution values - the last cached attribution values are passed correctly in the payload - */ const updatedCtx3 = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj3 = updatedCtx3?.event?.integrations[DESTINATION_INTEGRATION_NAME] @@ -128,6 +156,9 @@ describe('ajs-integration', () => { ) + /* + * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload + */ Object.defineProperty(window, 'location', { value: { search: '?some_fake_non_attribution_param=12345' @@ -135,9 +166,6 @@ describe('ajs-integration', () => { writable: true }) - /* - * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload - */ const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] @@ -152,6 +180,9 @@ describe('ajs-integration', () => { } ) + /* + * Finally we test with a completely new attribution parameter + */ Object.defineProperty(window, 'location', { value: { search: '?ttclid=uyiuyiuy' @@ -159,9 +190,6 @@ describe('ajs-integration', () => { writable: true }) - /* - * Finally we test with a completely new attribution parameter - */ const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] From 81bc61171748e1ecbb61d09dca30975c45e4a561 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 3 Nov 2025 15:36:21 +0000 Subject: [PATCH 06/38] moving stuff to shared --- .../src/amplitude}/constants.ts | 4 ++-- packages/actions-shared/src/amplitude/types.ts | 10 ++++++++++ packages/actions-shared/src/index.ts | 2 ++ .../src/autocaptureAttribution/functions.ts | 18 +++++++++--------- .../src/autocaptureAttribution/index.ts | 13 +++++++++---- .../src/autocaptureAttribution/types.ts | 5 ----- 6 files changed, 32 insertions(+), 20 deletions(-) rename packages/{browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution => actions-shared/src/amplitude}/constants.ts (66%) create mode 100644 packages/actions-shared/src/amplitude/types.ts delete mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/types.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts b/packages/actions-shared/src/amplitude/constants.ts similarity index 66% rename from packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts rename to packages/actions-shared/src/amplitude/constants.ts index 9d01e230cdd..e33d48b0112 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/constants.ts +++ b/packages/actions-shared/src/amplitude/constants.ts @@ -1,4 +1,4 @@ -export const KEYS = [ +export const AMPLITUDE_ATTRIBUTION_KEYS = [ 'utm_source', 'utm_medium', 'utm_campaign', @@ -18,4 +18,4 @@ export const KEYS = [ 'twclid' ] as const -export const ATTRIBUTION_STORAGE_KEY = 'amplitude-attribution-params' \ No newline at end of file +export const AMPLITUDE_ATTRIBUTION_STORAGE_KEY = 'amplitude-attribution-params' \ No newline at end of file diff --git a/packages/actions-shared/src/amplitude/types.ts b/packages/actions-shared/src/amplitude/types.ts new file mode 100644 index 00000000000..bac1d680f1d --- /dev/null +++ b/packages/actions-shared/src/amplitude/types.ts @@ -0,0 +1,10 @@ +import { AMPLITUDE_ATTRIBUTION_KEYS } from '../amplitude/constants' + +export type AmplitudeAttributionKey = typeof AMPLITUDE_ATTRIBUTION_KEYS[number] + +export type AmplitudeAttributionValues = Record + +export type AmplitudeAttributionComparison = { + old: Partial + new: Partial +} \ No newline at end of file diff --git a/packages/actions-shared/src/index.ts b/packages/actions-shared/src/index.ts index 1705cf33342..dd42eac24a6 100644 --- a/packages/actions-shared/src/index.ts +++ b/packages/actions-shared/src/index.ts @@ -7,3 +7,5 @@ export * from './friendbuy/sharedPurchase' export * from './friendbuy/sharedSignUp' export * from './friendbuy/util' export * from './engage/utils' +export * from './amplitude/types' +export * from './amplitude/constants' diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts index e15149a1268..38b1bd16db0 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts @@ -1,9 +1,9 @@ import { UniversalStorage } from '@segment/analytics-next' -import type { AttributionValues } from './types' -import { KEYS, ATTRIBUTION_STORAGE_KEY } from './constants' +import type { AmplitudeAttributionValues } from '@segment/actions-shared/src/amplitude/types' +import { AMPLITUDE_ATTRIBUTION_KEYS, AMPLITUDE_ATTRIBUTION_STORAGE_KEY } from '@segment/actions-shared' -export function getAttributionsFromURL(queryString: string): Partial { +export function getAttributionsFromURL(queryString: string): Partial { if (!queryString){ return {} } @@ -11,17 +11,17 @@ export function getAttributionsFromURL(queryString: string): Partial [key, urlParams.get(key)] as const) .filter(([, value]) => value !== null) - ) as Partial + ) as Partial } -export function getAttributionsFromStorage(storage: UniversalStorage>>): Partial { - const values = storage.get(ATTRIBUTION_STORAGE_KEY) +export function getAttributionsFromStorage(storage: UniversalStorage>>): Partial { + const values = storage.get(AMPLITUDE_ATTRIBUTION_STORAGE_KEY) return values ?? {} } -export function setAttributionsInStorage(storage: UniversalStorage>>, attributions: Partial): void { - storage.set(ATTRIBUTION_STORAGE_KEY, attributions) +export function setAttributionsInStorage(storage: UniversalStorage>>, attributions: Partial): void { + storage.set(AMPLITUDE_ATTRIBUTION_STORAGE_KEY, attributions) } \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index c0a147ab3fc..4eed657b626 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -4,7 +4,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' -import { AttributionValues } from './types' +import { AmplitudeAttributionValues, AmplitudeAttributionComparison } from '@segment/actions-shared' import { DESTINATION_INTEGRATION_NAME } from '../constants' const action: BrowserActionDefinition = { @@ -16,15 +16,20 @@ const action: BrowserActionDefinition = { lifecycleHook: 'enrichment', perform: (_, { context, analytics }) => { const urlAttributions = getAttributionsFromURL(window.location.search) - const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) + const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) + const comparison: AmplitudeAttributionComparison = { + old: cachedAttributions, + new: urlAttributions + } + if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) - context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { old: cachedAttributions, new: urlAttributions }) + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, comparison) } if(Object.entries(urlAttributions).length >0) { - setAttributionsInStorage(analytics.storage as UniversalStorage>>, urlAttributions) + setAttributionsInStorage(analytics.storage as UniversalStorage>>, urlAttributions) } return } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/types.ts deleted file mode 100644 index 2a629b30084..00000000000 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { KEYS } from './constants' - -export type AttributionKey = typeof KEYS[number] - -export type AttributionValues = Record From fe3b2a23f45d1def599037b344e6700ae1d1bd5f Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 14:52:50 +0000 Subject: [PATCH 07/38] saving progress --- .../actions-shared/src/amplitude/types.ts | 7 +- .../__tests__/index.test.ts | 271 ++++++++++++------ .../src/autocaptureAttribution/functions.ts | 1 + .../autocaptureAttribution/generated-types.ts | 4 +- .../src/autocaptureAttribution/index.ts | 58 +++- 5 files changed, 240 insertions(+), 101 deletions(-) diff --git a/packages/actions-shared/src/amplitude/types.ts b/packages/actions-shared/src/amplitude/types.ts index bac1d680f1d..2a41fe0a478 100644 --- a/packages/actions-shared/src/amplitude/types.ts +++ b/packages/actions-shared/src/amplitude/types.ts @@ -2,9 +2,4 @@ import { AMPLITUDE_ATTRIBUTION_KEYS } from '../amplitude/constants' export type AmplitudeAttributionKey = typeof AMPLITUDE_ATTRIBUTION_KEYS[number] -export type AmplitudeAttributionValues = Record - -export type AmplitudeAttributionComparison = { - old: Partial - new: Partial -} \ No newline at end of file +export type AmplitudeAttributionValues = Record \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index 73c8a0639f4..d151e1a959c 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -51,52 +51,100 @@ describe('ajs-integration', () => { */ const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj).toEqual( - { - autocapture_attribution: { - new: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - }, - old: {} - } + expect(ampIntegrationsObj).toEqual({ + autocapture_attribution: { + set: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + }, + set_once: { + dclid: "", + fbclid: "", + gbraid: "gbraid5678", + gclid: "gclid1234", + ko_clickid: "", + li_fat_id: "", + msclkid: "", + rtd_cid: "", + ttclid: "", + twclid: "", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_id: "", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + wbraid: "" + }, + unset: [ + "utm_id", + "dclid", + "fbclid", + "wbraid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] } - ) + }) /* * Then we test the what happens when exact same attributions from the URL are captured and added to the next event */ const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj1).toEqual( - { - autocapture_attribution: { - new: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - }, - old: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - } - } + expect(ampIntegrationsObj1).toEqual({ + autocapture_attribution: { + set: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + }, + set_once: { + dclid: "", + fbclid: "", + gbraid: "gbraid5678", + gclid: "gclid1234", + ko_clickid: "", + li_fat_id: "", + msclkid: "", + rtd_cid: "", + ttclid: "", + twclid: "", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_id: "", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + wbraid: "" + }, + unset: [ + "utm_id", + "dclid", + "fbclid", + "wbraid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] } - ) + }) /* @@ -115,18 +163,46 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj2).toEqual( { autocapture_attribution: { - new: { - utm_source: "email" + set: { + utm_source: "email", }, - old: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - } + set_once: { + dclid: "", + fbclid: "", + gbraid: "", + gclid: "", + ko_clickid: "", + li_fat_id: "", + msclkid: "", + rtd_cid: "", + ttclid: "", + twclid: "", + utm_campaign: "", + utm_content: "", + utm_id: "", + utm_medium: "", + utm_source: "email", + utm_term: "", + wbraid: "" + }, + unset: [ + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "utm_id", + "dclid", + "fbclid", + "gbraid", + "wbraid", + "gclid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] } } ) @@ -147,10 +223,46 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj3).toEqual( { autocapture_attribution: { - new: {}, - old: { - utm_source: "email" - } + set: { + utm_source: "email", + }, + set_once: { + dclid: "", + fbclid: "", + gbraid: "", + gclid: "", + ko_clickid: "", + li_fat_id: "", + msclkid: "", + rtd_cid: "", + ttclid: "", + twclid: "", + utm_campaign: "", + utm_content: "", + utm_id: "", + utm_medium: "", + utm_source: "email", + utm_term: "", + wbraid: "" + }, + unset: [ + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "utm_id", + "dclid", + "fbclid", + "gbraid", + "wbraid", + "gclid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] } } ) @@ -171,40 +283,35 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj4).toEqual( { - autocapture_attribution: { - new: {}, - old: { - utm_source: "email" - } - } + } ) - /* - * Finally we test with a completely new attribution parameter - */ - Object.defineProperty(window, 'location', { - value: { - search: '?ttclid=uyiuyiuy' - }, - writable: true - }) + // /* + // * Finally we test with a completely new attribution parameter + // */ + // Object.defineProperty(window, 'location', { + // value: { + // search: '?ttclid=uyiuyiuy' + // }, + // writable: true + // }) - const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] + // const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) + // const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj5).toEqual( - { - autocapture_attribution: { - new: { - ttclid: "uyiuyiuy" - }, - old: { - utm_source: "email" - } - } - } - ) + // expect(ampIntegrationsObj5).toEqual( + // { + // autocapture_attribution: { + // new: { + // ttclid: "uyiuyiuy" + // }, + // old: { + // utm_source: "email" + // } + // } + // } + // ) }) }) \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts index 38b1bd16db0..bfd85aaa3ee 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts @@ -19,6 +19,7 @@ export function getAttributionsFromURL(queryString: string): Partial>>): Partial { const values = storage.get(AMPLITUDE_ATTRIBUTION_STORAGE_KEY) + console.log('Retrieved attributions from storage:', values) return values ?? {} } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts index 74523ad652e..0b960fbf637 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts @@ -1,3 +1,5 @@ // Generated file. DO NOT MODIFY IT BY HAND. -export interface Payload {} \ No newline at end of file +export interface Payload { + excludeReferrers?: string[] +} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index 4eed657b626..df1bc761f81 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -4,7 +4,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' -import { AmplitudeAttributionValues, AmplitudeAttributionComparison } from '@segment/actions-shared' +import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeAttributionKey } from '@segment/actions-shared' import { DESTINATION_INTEGRATION_NAME } from '../constants' const action: BrowserActionDefinition = { @@ -12,25 +12,59 @@ const action: BrowserActionDefinition = { description: 'Captures attribution details from the URL and attaches them to every Amplitude browser based event.', platform: 'web', defaultSubscription: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', - fields: {}, + fields: { + excludeReferrers: { + label: 'Exclude Referrers', + description: 'A list of hostnames to ignore when capturing attribution data. If the current page referrer matches any of these hostnames, no attribution data will be captured from the URL.', + type: 'string', + required: false, + multiple: true + } + }, lifecycleHook: 'enrichment', - perform: (_, { context, analytics }) => { - const urlAttributions = getAttributionsFromURL(window.location.search) - const cachedAttributions = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) - - const comparison: AmplitudeAttributionComparison = { - old: cachedAttributions, - new: urlAttributions + perform: (_, { context, payload, analytics }) => { + const referrer = document.referrer + const referrerDomain = referrer ? new URL(referrer).hostname : '' + const { excludeReferrers } = payload + const isExcluded = excludeReferrers?.includes(referrerDomain) + const current = isExcluded ? {} : getAttributionsFromURL(window.location.search) + const previous = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) + const setOnce: Partial = {} + const set: Partial = {} + const unset: AmplitudeAttributionKey[] = [] + + const currentPageHasAttribution = current && Object.values(current).some(v => typeof v === 'string' && v.length > 0) + + if (!currentPageHasAttribution) { + return } + AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { + // Always set_once the current values from the URL if there is at least one attribution value present + setOnce[key] = current[key] ?? "" + if(current[key]){ + // If there are any attribution values on the page, set them + set[key] = current[key] + } + else if(previous[key]){ + // if there are any previous attribution values which are not in current URL, unset them + unset.push(key) + } + }) + if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) - context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, comparison) + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { + set_once: setOnce, + set: set, + unset: unset + }) } - if(Object.entries(urlAttributions).length >0) { - setAttributionsInStorage(analytics.storage as UniversalStorage>>, urlAttributions) + if(Object.entries(current).length >0) { + setAttributionsInStorage(analytics.storage as UniversalStorage>>, current) } + return } } From fc7c026445a6368aa672dac471ea82ac6f43ac91 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 17:18:21 +0000 Subject: [PATCH 08/38] unit tests for front end code --- .../__tests__/index.test.ts | 158 +++++++----------- .../src/autocaptureAttribution/functions.ts | 1 - .../src/autocaptureAttribution/index.ts | 35 ++-- 3 files changed, 79 insertions(+), 115 deletions(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index d151e1a959c..b8c76613306 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -47,12 +47,13 @@ describe('ajs-integration', () => { }) /* - * First we test that the attributions from the URL are captured and added to the event + * First event on the page with attribution values will be transmitted with set and set_once values */ const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] expect(ampIntegrationsObj).toEqual({ autocapture_attribution: { + autocapture_attribution_enabled: true, set: { gbraid: "gbraid5678", gclid: "gclid1234", @@ -97,58 +98,22 @@ describe('ajs-integration', () => { }) /* - * Then we test the what happens when exact same attributions from the URL are captured and added to the next event + * Second event on the same page with attribution values will be transmitted without set and set_once values */ const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] expect(ampIntegrationsObj1).toEqual({ autocapture_attribution: { - set: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - }, - set_once: { - dclid: "", - fbclid: "", - gbraid: "gbraid5678", - gclid: "gclid1234", - ko_clickid: "", - li_fat_id: "", - msclkid: "", - rtd_cid: "", - ttclid: "", - twclid: "", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_id: "", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - wbraid: "" - }, - unset: [ - "utm_id", - "dclid", - "fbclid", - "wbraid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid" - ] + autocapture_attribution_enabled: true, + set_once: {}, + set: {}, + unset: [] } }) /* - * Then we test that the new attributes from the URL are captured, the old cached values are retrieved + * A new URL should result in updated set and unset values being sent in the payload */ Object.defineProperty(window, 'location', { value: { @@ -163,6 +128,7 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj2).toEqual( { autocapture_attribution: { + autocapture_attribution_enabled: true, set: { utm_source: "email", }, @@ -208,7 +174,7 @@ describe('ajs-integration', () => { ) /* - * Then we test when there are no new URL attribution values - the last cached attribution values are passed correctly in the payload + * Next a new page load happens which does not have any valid attribution values. No attribution values should be sent in the payload */ Object.defineProperty(window, 'location', { value: { @@ -223,8 +189,58 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj3).toEqual( { autocapture_attribution: { + autocapture_attribution_enabled: true, + set: {}, + set_once: {}, + unset: [] + } + } + ) + + + /* + * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?some_fake_non_attribution_param=12345' + }, + writable: true + }) + + const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj4).toEqual( + { + autocapture_attribution: { + autocapture_attribution_enabled: true, + set: {}, + set_once: {}, + unset: [] + } + } + ) + + /* + * Finally we test with a completely new attribution parameter + */ + Object.defineProperty(window, 'location', { + value: { + search: '?ttclid=uyiuyiuy' + }, + writable: true + }) + + const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj5).toEqual( + { + autocapture_attribution: { + autocapture_attribution_enabled: true, set: { - utm_source: "email", + ttclid: "uyiuyiuy" }, set_once: { dclid: "", @@ -235,17 +251,18 @@ describe('ajs-integration', () => { li_fat_id: "", msclkid: "", rtd_cid: "", - ttclid: "", + ttclid: "uyiuyiuy", twclid: "", utm_campaign: "", utm_content: "", utm_id: "", utm_medium: "", - utm_source: "email", + utm_source: "", utm_term: "", wbraid: "" }, unset: [ + "utm_source", "utm_medium", "utm_campaign", "utm_term", @@ -260,58 +277,11 @@ describe('ajs-integration', () => { "li_fat_id", "msclkid", "rtd_cid", - "ttclid", "twclid" ] } } ) - - /* - * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?some_fake_non_attribution_param=12345' - }, - writable: true - }) - - const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj4).toEqual( - { - - } - ) - - // /* - // * Finally we test with a completely new attribution parameter - // */ - // Object.defineProperty(window, 'location', { - // value: { - // search: '?ttclid=uyiuyiuy' - // }, - // writable: true - // }) - - // const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) - // const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - // expect(ampIntegrationsObj5).toEqual( - // { - // autocapture_attribution: { - // new: { - // ttclid: "uyiuyiuy" - // }, - // old: { - // utm_source: "email" - // } - // } - // } - // ) - }) }) \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts index bfd85aaa3ee..38b1bd16db0 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts @@ -19,7 +19,6 @@ export function getAttributionsFromURL(queryString: string): Partial>>): Partial { const values = storage.get(AMPLITUDE_ATTRIBUTION_STORAGE_KEY) - console.log('Retrieved attributions from storage:', values) return values ?? {} } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index df1bc761f81..c5a9cbdf30a 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -6,6 +6,7 @@ import type { Payload } from './generated-types' import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeAttributionKey } from '@segment/actions-shared' import { DESTINATION_INTEGRATION_NAME } from '../constants' +import isEqual from 'lodash/isEqual' const action: BrowserActionDefinition = { title: 'Autocapture Attribution Plugin', @@ -32,39 +33,33 @@ const action: BrowserActionDefinition = { const setOnce: Partial = {} const set: Partial = {} const unset: AmplitudeAttributionKey[] = [] - const currentPageHasAttribution = current && Object.values(current).some(v => typeof v === 'string' && v.length > 0) - if (!currentPageHasAttribution) { - return + if (currentPageHasAttribution && !isEqual(current, previous)){ + AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { + setOnce[key] = current[key] ?? "" + if(current[key]){ + set[key] = current[key] + } + else{ + unset.push(key) + } + }) + if(Object.entries(current).length >0) { + setAttributionsInStorage(analytics.storage as UniversalStorage>>, current) + } } - AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { - // Always set_once the current values from the URL if there is at least one attribution value present - setOnce[key] = current[key] ?? "" - if(current[key]){ - // If there are any attribution values on the page, set them - set[key] = current[key] - } - else if(previous[key]){ - // if there are any previous attribution values which are not in current URL, unset them - unset.push(key) - } - }) - if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { + autocapture_attribution_enabled: true, set_once: setOnce, set: set, unset: unset }) } - if(Object.entries(current).length >0) { - setAttributionsInStorage(analytics.storage as UniversalStorage>>, current) - } - return } } From 4c213c7c7aa79d8f386b420a8c3665b265fd16a2 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 17:47:11 +0000 Subject: [PATCH 09/38] more progresss --- .../actions-shared/src/amplitude/constants.ts | 2 ++ .../__tests__/index.test.ts | 12 +++---- .../autocaptureAttribution/generated-types.ts | 7 ++-- .../src/autocaptureAttribution/index.ts | 8 ++--- .../amplitude/autocaptueAttributions.ts | 1 + .../amplitude/logEventV2/generated-types.ts | 22 +++++++++++++ .../amplitude/logEventV2/index.ts | 32 +++++++++++++++++++ 7 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts diff --git a/packages/actions-shared/src/amplitude/constants.ts b/packages/actions-shared/src/amplitude/constants.ts index e33d48b0112..4b53ee47f84 100644 --- a/packages/actions-shared/src/amplitude/constants.ts +++ b/packages/actions-shared/src/amplitude/constants.ts @@ -1,4 +1,6 @@ export const AMPLITUDE_ATTRIBUTION_KEYS = [ + 'referrer', + 'referring_domain', 'utm_source', 'utm_medium', 'utm_campaign', diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index b8c76613306..4664e96b9a1 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -53,7 +53,7 @@ describe('ajs-integration', () => { const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] expect(ampIntegrationsObj).toEqual({ autocapture_attribution: { - autocapture_attribution_enabled: true, + enabled: true, set: { gbraid: "gbraid5678", gclid: "gclid1234", @@ -104,7 +104,7 @@ describe('ajs-integration', () => { const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] expect(ampIntegrationsObj1).toEqual({ autocapture_attribution: { - autocapture_attribution_enabled: true, + enabled: true, set_once: {}, set: {}, unset: [] @@ -128,7 +128,7 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj2).toEqual( { autocapture_attribution: { - autocapture_attribution_enabled: true, + enabled: true, set: { utm_source: "email", }, @@ -189,7 +189,7 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj3).toEqual( { autocapture_attribution: { - autocapture_attribution_enabled: true, + enabled: true, set: {}, set_once: {}, unset: [] @@ -214,7 +214,7 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj4).toEqual( { autocapture_attribution: { - autocapture_attribution_enabled: true, + enabled: true, set: {}, set_once: {}, unset: [] @@ -238,7 +238,7 @@ describe('ajs-integration', () => { expect(ampIntegrationsObj5).toEqual( { autocapture_attribution: { - autocapture_attribution_enabled: true, + enabled: true, set: { ttclid: "uyiuyiuy" }, diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts index 0b960fbf637..29e6cfb34b1 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts @@ -1,5 +1,8 @@ // Generated file. DO NOT MODIFY IT BY HAND. export interface Payload { - excludeReferrers?: string[] -} \ No newline at end of file + /** + * A list of hostnames to ignore when capturing attribution data. If the current page referrer matches any of these hostnames, no attribution data will be captured from the URL. + */ + excludeReferrers?: string[] +} diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index c5a9cbdf30a..9168d8ba121 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -25,10 +25,10 @@ const action: BrowserActionDefinition = { lifecycleHook: 'enrichment', perform: (_, { context, payload, analytics }) => { const referrer = document.referrer - const referrerDomain = referrer ? new URL(referrer).hostname : '' + const referringDomain = referrer ? new URL(referrer).hostname : '' const { excludeReferrers } = payload - const isExcluded = excludeReferrers?.includes(referrerDomain) - const current = isExcluded ? {} : getAttributionsFromURL(window.location.search) + const isExcluded = excludeReferrers?.includes(referringDomain) + const current: Partial = isExcluded ? {} : {...getAttributionsFromURL(window.location.search), referrer, referring_domain: referringDomain} const previous = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) const setOnce: Partial = {} const set: Partial = {} @@ -53,7 +53,7 @@ const action: BrowserActionDefinition = { if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { - autocapture_attribution_enabled: true, + enabled: true, set_once: setOnce, set: set, unset: unset diff --git a/packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts b/packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts new file mode 100644 index 00000000000..a1a2643db79 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts @@ -0,0 +1 @@ +export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 787b698616a..8b853b03dd1 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -210,6 +210,28 @@ export interface Payload { add?: { [k: string]: unknown } + /** + * Utility field used to detect if Autocapture Attribution Plugin is enabled. + */ + autocaptureAttributionEnabled?: boolean + /** + * Utility field which is used to funnel autocaptured attributon data which will be set in Amplitude. + */ + autocaptureAttributionSet?: { + [k: string]: unknown + } + /** + * Utility field which is used to funnel autocaptured attributon data which will be set_once in Amplitude. + */ + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } + /** + * Utility field which is used to funnel autocaptured attributon data which will be unset in Amplitude. + */ + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index ef20b455231..c7ffcad2f9a 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -8,6 +8,8 @@ import { parseUserAgentProperties } from '../user-agent' import type { Payload } from './generated-types' import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' +import { DESTINATION_INTEGRATION_NAME } from '../autocaptueAttributions' +import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' export interface AmplitudeEvent extends Omit { library?: string @@ -175,6 +177,34 @@ const action: ActionDefinition = { additionalProperties: true, defaultObjectUI: 'keyvalue' }, + autocaptureAttributionEnabled: { + label: 'Autocapture Attribution Enabled', + description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', + type: 'boolean', + default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, + readOnly: true + }, + autocaptureAttributionSet: { + label: 'Autocapture Attribution Set', + description: 'Utility field used to detect if any attribution values need to be set.', + type: 'object', + default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, + readOnly: true + }, + autocaptureAttributionSetOnce: { + label: 'Autocapture Attribution Set Once', + description: 'Utility field used to detect if any attribution values need to be set_once.', + type: 'object', + default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, + readOnly: true + }, + autocaptureAttributionUnset: { + label: 'Autocapture Attribution Unset', + description: 'Utility field used to detect if any attribution values need to be unset.', + type: 'object', + default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, + readOnly: true + }, use_batch_endpoint: { label: 'Use Batch Endpoint', description: @@ -260,6 +290,8 @@ const action: ActionDefinition = { } } + + setUserProperties('$setOnce', setOnce) setUserProperties('$set', setAlways) setUserProperties('$add', add) From 45f7a717bf406ebef04c6be817279bd4402beef7 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 18:23:15 +0000 Subject: [PATCH 10/38] server side code for logEventV2 --- .../src/destinations/amplitude/logEventV2/index.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index c7ffcad2f9a..3986cdd3ce3 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -9,7 +9,6 @@ import type { Payload } from './generated-types' import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' import { DESTINATION_INTEGRATION_NAME } from '../autocaptueAttributions' -import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' export interface AmplitudeEvent extends Omit { library?: string @@ -256,6 +255,10 @@ const action: ActionDefinition = { setOnce, setAlways, add, + autocaptureAttributionEnabled, + autocaptureAttributionSet, + autocaptureAttributionSetOnce, + autocaptureAttributionUnset, ...rest } = omit(payload, revenueKeys) const properties = rest as AmplitudeEvent @@ -282,7 +285,7 @@ const action: ActionDefinition = { } const setUserProperties = ( - name: '$setOnce' | '$set' | '$add', + name: '$setOnce' | '$set' | '$add' | '$unset', obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] ) => { if (compact(obj)) { @@ -290,11 +293,10 @@ const action: ActionDefinition = { } } - - - setUserProperties('$setOnce', setOnce) - setUserProperties('$set', setAlways) + setUserProperties('$setOnce', autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce} : setOnce) + setUserProperties('$set', autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet} : setAlways) setUserProperties('$add', add) + setUserProperties('$unset', autocaptureAttributionEnabled ? { ...autocaptureAttributionUnset} : {}) const events: AmplitudeEvent[] = [ { From bfed026327b1958aa2909a6b8d36c3f72d3b27c4 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 18:27:12 +0000 Subject: [PATCH 11/38] minor tweak --- .../src/destinations/amplitude/logEventV2/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 3986cdd3ce3..149773245b3 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -286,7 +286,7 @@ const action: ActionDefinition = { const setUserProperties = ( name: '$setOnce' | '$set' | '$add' | '$unset', - obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] + obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] | Payload['autocaptureAttributionUnset'] ) => { if (compact(obj)) { properties.user_properties = { ...properties.user_properties, [name]: obj } @@ -295,8 +295,8 @@ const action: ActionDefinition = { setUserProperties('$setOnce', autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce} : setOnce) setUserProperties('$set', autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet} : setAlways) - setUserProperties('$add', add) setUserProperties('$unset', autocaptureAttributionEnabled ? { ...autocaptureAttributionUnset} : {}) + setUserProperties('$add', add) const events: AmplitudeEvent[] = [ { From f7d4aaca1a42aa5a7436c6b96eeb3a48d63d6260 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 18:47:34 +0000 Subject: [PATCH 12/38] progress --- .../__tests__/index.test.ts | 114 ++++++++++-------- .../src/autocaptureAttribution/index.ts | 6 +- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index 4664e96b9a1..1e3e9a24e11 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -64,25 +64,29 @@ describe('ajs-integration', () => { utm_term: "running shoes", }, set_once: { - dclid: "", - fbclid: "", - gbraid: "gbraid5678", - gclid: "gclid1234", - ko_clickid: "", - li_fat_id: "", - msclkid: "", - rtd_cid: "", - ttclid: "", - twclid: "", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_id: "", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - wbraid: "" + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "gbraid5678", + initial_gclid: "gclid1234", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer: "", + initial_referring_domain: "", + initial_rtd_cid: "", + initial_ttclid: "", + initial_twclid: "", + initial_utm_campaign: "spring_sale", + initial_utm_content: "ad1", + initial_utm_id: "", + initial_utm_medium: "cpc", + initial_utm_source: "google", + initial_utm_term: "running shoes", + initial_wbraid: "" }, unset: [ + "referrer", + "referring_domain", "utm_id", "dclid", "fbclid", @@ -133,25 +137,29 @@ describe('ajs-integration', () => { utm_source: "email", }, set_once: { - dclid: "", - fbclid: "", - gbraid: "", - gclid: "", - ko_clickid: "", - li_fat_id: "", - msclkid: "", - rtd_cid: "", - ttclid: "", - twclid: "", - utm_campaign: "", - utm_content: "", - utm_id: "", - utm_medium: "", - utm_source: "email", - utm_term: "", - wbraid: "" + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer:"", + initial_referring_domain: "", + initial_rtd_cid: "", + initial_ttclid: "", + initial_twclid: "", + initial_utm_campaign: "", + initial_utm_content: "", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "email", + initial_utm_term: "", + initial_wbraid: "" }, unset: [ + 'referrer', + 'referring_domain', "utm_medium", "utm_campaign", "utm_term", @@ -243,25 +251,29 @@ describe('ajs-integration', () => { ttclid: "uyiuyiuy" }, set_once: { - dclid: "", - fbclid: "", - gbraid: "", - gclid: "", - ko_clickid: "", - li_fat_id: "", - msclkid: "", - rtd_cid: "", - ttclid: "uyiuyiuy", - twclid: "", - utm_campaign: "", - utm_content: "", - utm_id: "", - utm_medium: "", - utm_source: "", - utm_term: "", - wbraid: "" + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer: "", + initial_referring_domain: "", + initial_rtd_cid: "", + initial_ttclid: "uyiuyiuy", + initial_twclid: "", + initial_utm_campaign: "", + initial_utm_content: "", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "", + initial_utm_term: "", + initial_wbraid: "" }, unset: [ + "referrer", + "referring_domain", "utm_source", "utm_medium", "utm_campaign", diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index 9168d8ba121..7a718da8854 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -4,7 +4,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' -import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeAttributionKey } from '@segment/actions-shared' +import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeAttributionKey, AmplitudeSetOnceAttributionValues } from '@segment/actions-shared' import { DESTINATION_INTEGRATION_NAME } from '../constants' import isEqual from 'lodash/isEqual' @@ -30,14 +30,14 @@ const action: BrowserActionDefinition = { const isExcluded = excludeReferrers?.includes(referringDomain) const current: Partial = isExcluded ? {} : {...getAttributionsFromURL(window.location.search), referrer, referring_domain: referringDomain} const previous = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) - const setOnce: Partial = {} + const setOnce: Partial = {} const set: Partial = {} const unset: AmplitudeAttributionKey[] = [] const currentPageHasAttribution = current && Object.values(current).some(v => typeof v === 'string' && v.length > 0) if (currentPageHasAttribution && !isEqual(current, previous)){ AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { - setOnce[key] = current[key] ?? "" + setOnce[`initial_${key}`] = current[key] ?? "" if(current[key]){ set[key] = current[key] } From 981531f603e6ac702556b4fdfdd6eef56832eeb3 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 12 Nov 2025 18:53:36 +0000 Subject: [PATCH 13/38] starting server side unit tests --- .../actions-shared/src/amplitude/types.ts | 6 +- .../__tests__/autocapture-attribution.test.ts | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts diff --git a/packages/actions-shared/src/amplitude/types.ts b/packages/actions-shared/src/amplitude/types.ts index 2a41fe0a478..a7b5d50a69a 100644 --- a/packages/actions-shared/src/amplitude/types.ts +++ b/packages/actions-shared/src/amplitude/types.ts @@ -2,4 +2,8 @@ import { AMPLITUDE_ATTRIBUTION_KEYS } from '../amplitude/constants' export type AmplitudeAttributionKey = typeof AMPLITUDE_ATTRIBUTION_KEYS[number] -export type AmplitudeAttributionValues = Record \ No newline at end of file +export type AmplitudeSetOnceAttributionKey = `initial_${AmplitudeAttributionKey}` + +export type AmplitudeAttributionValues = Record + +export type AmplitudeSetOnceAttributionValues = Record \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts new file mode 100644 index 00000000000..3ba78f9b5a6 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -0,0 +1,66 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Amplitude from '../index' + +const testDestination = createTestIntegration(Amplitude) +const timestamp = '2021-08-17T15:21:15.449Z' + +describe('Amplitude', () => { + + describe('logEvent V2', () => { + + it('correctly handles the default mappings for setOnce, setAlways, and add', async () => { + nock('https://api2.amplitude.com/2').post('/httpapi').reply(200, {}) + + const event = createTestEvent({ + timestamp, + event: 'Test Event', + traits: { + 'some-trait-key': 'some-trait-value' + }, + context: { + page: { + referrer: 'some-referrer' + }, + campaign: { + name: 'TPS Innovation Newsletter', + source: 'Newsletter', + medium: 'email', + term: 'tps reports', + content: 'image link' + } + } + }) + + const responses = await testDestination.testAction('logEventV2', { event, useDefaultMappings: true }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + api_key: undefined, + events: expect.arrayContaining([ + expect.objectContaining({ + user_properties: expect.objectContaining({ + $set: { + referrer: 'some-referrer', + utm_campaign: 'TPS Innovation Newsletter', + utm_content: 'image link', + utm_medium: 'email', + utm_source: 'Newsletter', + utm_term: 'tps reports' + }, + $setOnce: { + initial_referrer: 'some-referrer', + initial_utm_campaign: 'TPS Innovation Newsletter', + initial_utm_content: 'image link', + initial_utm_medium: 'email', + initial_utm_source: 'Newsletter', + initial_utm_term: 'tps reports' + } + }) + }) + ]) + }) + }) + }) + +}) From 20b60898fd060704c4c6494ad1aae9d7cfe92ab9 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 13 Nov 2025 11:07:35 +0000 Subject: [PATCH 14/38] more progress --- packages/core/src/segment-event.ts | 4 +- .../__tests__/autocapture-attribution.test.ts | 150 ++++++++++++++---- .../amplitude/logEventV2/generated-types.ts | 4 +- .../amplitude/logEventV2/index.ts | 29 +++- 4 files changed, 146 insertions(+), 41 deletions(-) diff --git a/packages/core/src/segment-event.ts b/packages/core/src/segment-event.ts index dc92d03075b..7862ec7603d 100644 --- a/packages/core/src/segment-event.ts +++ b/packages/core/src/segment-event.ts @@ -1,4 +1,4 @@ -import { JSONValue } from './json-object' +import { JSONObject, JSONValue } from './json-object' export type ID = string | null | undefined @@ -14,7 +14,7 @@ interface CompactMetric { export type Integrations = { All?: boolean - [integration: string]: boolean | undefined + [integration: string]: boolean | undefined | JSONObject } export type Options = { diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts index 3ba78f9b5a6..d4a4fdca185 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -1,17 +1,63 @@ import nock from 'nock' import { createTestEvent, createTestIntegration } from '@segment/actions-core' import Amplitude from '../index' +import {AmplitudeAttributionValues, AmplitudeSetOnceAttributionValues, AmplitudeAttributionKey} from '@segment/actions-shared' const testDestination = createTestIntegration(Amplitude) const timestamp = '2021-08-17T15:21:15.449Z' describe('Amplitude', () => { - describe('logEvent V2', () => { - - it('correctly handles the default mappings for setOnce, setAlways, and add', async () => { + it('correctly handles autocapture attribution values passed in integrations object', async () => { nock('https://api2.amplitude.com/2').post('/httpapi').reply(200, {}) + const set_once: AmplitudeSetOnceAttributionValues = { + initial_referrer: 'initial-referrer-from-integrations-object', + initial_utm_campaign: 'initial-utm-campaign-from-integrations-object', + initial_utm_content: 'initial-utm-content-from-integrations-object', + initial_utm_medium: '', + initial_utm_source: 'initial-utm-source-from-integrations-object', + initial_utm_term: 'initial-utm-term-from-integrations-object', + initial_gclid: 'initial-gclid-from-integrations-object', + initial_fbclid: '', + initial_dclid: '', + initial_gbraid: '', + initial_wbraid: '', + initial_ko_clickid: '', + initial_li_fat_id: '', + initial_msclkid: '', + initial_referring_domain: 'initial-referring-domain-from-integrations-object', + initial_rtd_cid: '', + initial_ttclid: '', + initial_twclid: '', + initial_utm_id: '' + } + + const set: Partial = { + referrer: 'referrer-from-integrations-object', + utm_campaign: 'utm-campaign-from-integrations-object', + utm_content: 'utm-content-from-integrations-object', + utm_source: 'utm-source-from-integrations-object', + utm_term: 'utm-term-from-integrations-object', + gclid: 'gclid-from-integrations-object', + referring_domain: 'referring-domain-from-integrations-object' + } + + const unset: AmplitudeAttributionKey[] = [ + 'utm_medium', + 'fbclid', + 'dclid', + 'gbraid', + 'wbraid', + 'ko_clickid', + 'li_fat_id', + 'msclkid', + 'rtd_cid', + 'ttclid', + 'twclid', + 'utm_id' + ] + const event = createTestEvent({ timestamp, event: 'Test Event', @@ -19,15 +65,25 @@ describe('Amplitude', () => { 'some-trait-key': 'some-trait-value' }, context: { + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + enabled: true, + set_once, + set, + unset + } + } + }, page: { - referrer: 'some-referrer' + referrer: 'referrer-from-page-context' }, campaign: { - name: 'TPS Innovation Newsletter', - source: 'Newsletter', - medium: 'email', - term: 'tps reports', - content: 'image link' + name: 'campaign-name-from-campaign-context', + source: 'campaign-source-from-campaign-context', + medium: 'campaign-medium-from-campaign-context', + term: 'campaign-term-from-campaign-context', + content: 'campaign-content-from-campaign-context' } } }) @@ -37,30 +93,66 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].options.json).toMatchObject({ api_key: undefined, - events: expect.arrayContaining([ - expect.objectContaining({ - user_properties: expect.objectContaining({ + events: [ + { + device_id: "anonId1234", + event_properties: {}, + event_type: "Test Event", + library: "segment", + time: 1629213675449, + use_batch_endpoint: false, + user_id: "user1234", + user_properties: { $set: { - referrer: 'some-referrer', - utm_campaign: 'TPS Innovation Newsletter', - utm_content: 'image link', - utm_medium: 'email', - utm_source: 'Newsletter', - utm_term: 'tps reports' + gclid: "gclid-from-integrations-object", + referrer: "referrer-from-integrations-object", + referring_domain: "referring-domain-from-integrations-object", + utm_campaign: "utm-campaign-from-integrations-object", + utm_content: "utm-content-from-integrations-object", + utm_source: "utm-source-from-integrations-object", + utm_term: "utm-term-from-integrations-object", }, $setOnce: { - initial_referrer: 'some-referrer', - initial_utm_campaign: 'TPS Innovation Newsletter', - initial_utm_content: 'image link', - initial_utm_medium: 'email', - initial_utm_source: 'Newsletter', - initial_utm_term: 'tps reports' - } - }) - }) - ]) + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "initial-gclid-from-integrations-object", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer: "initial-referrer-from-integrations-object", + initial_referring_domain: "initial-referring-domain-from-integrations-object", + initial_rtd_cid: "", + initial_ttclid: "", + initial_twclid: "", + initial_utm_campaign: "initial-utm-campaign-from-integrations-object", + initial_utm_content: "initial-utm-content-from-integrations-object", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "initial-utm-source-from-integrations-object", + initial_utm_term: "initial-utm-term-from-integrations-object", + initial_wbraid: "", + }, + $unset: [ + "utm_medium", + "fbclid", + "dclid", + "gbraid", + "wbraid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid", + "utm_id", + ], + "some-trait-key": "some-trait-value", + }, + }, + ], + options: undefined, }) }) }) - }) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 8b853b03dd1..457d3bac7ff 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -229,9 +229,7 @@ export interface Payload { /** * Utility field which is used to funnel autocaptured attributon data which will be unset in Amplitude. */ - autocaptureAttributionUnset?: { - [k: string]: unknown - } + autocaptureAttributionUnset?: string[] /** * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 149773245b3..bf8aa718b47 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -9,6 +9,7 @@ import type { Payload } from './generated-types' import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' import { DESTINATION_INTEGRATION_NAME } from '../autocaptueAttributions' +import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' export interface AmplitudeEvent extends Omit { library?: string @@ -180,28 +181,29 @@ const action: ActionDefinition = { label: 'Autocapture Attribution Enabled', description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', type: 'boolean', - default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, readOnly: true }, autocaptureAttributionSet: { label: 'Autocapture Attribution Set', description: 'Utility field used to detect if any attribution values need to be set.', type: 'object', - default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, readOnly: true }, autocaptureAttributionSetOnce: { label: 'Autocapture Attribution Set Once', description: 'Utility field used to detect if any attribution values need to be set_once.', type: 'object', - default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, readOnly: true }, autocaptureAttributionUnset: { label: 'Autocapture Attribution Unset', description: 'Utility field used to detect if any attribution values need to be unset.', - type: 'object', - default: { '@path': `$.context.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, + type: 'string', + multiple: true, + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, readOnly: true }, use_batch_endpoint: { @@ -286,17 +288,30 @@ const action: ActionDefinition = { const setUserProperties = ( name: '$setOnce' | '$set' | '$add' | '$unset', - obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] | Payload['autocaptureAttributionUnset'] + obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] ) => { + console.log(name, obj) if (compact(obj)) { properties.user_properties = { ...properties.user_properties, [name]: obj } } } + if (autocaptureAttributionEnabled) { + // If autocapture attribution is enabled, we need to make sure that attribution keys are not sent from the setAlways and setOnce fields + for (const key of AMPLITUDE_ATTRIBUTION_KEYS) { + if( typeof setAlways === "object" && setAlways !== null){ + delete setAlways[key] + } + if(typeof setOnce === "object" && setOnce !== null){ + delete setOnce[`initial_${key}`] + } + } + } + setUserProperties('$setOnce', autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce} : setOnce) setUserProperties('$set', autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet} : setAlways) - setUserProperties('$unset', autocaptureAttributionEnabled ? { ...autocaptureAttributionUnset} : {}) setUserProperties('$add', add) + properties.user_properties = { ...properties.user_properties, ['$unset']: autocaptureAttributionUnset || []} const events: AmplitudeEvent[] = [ { From 2d8b38758e8f754eef52c31398ef4ffa52b5640a Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 13 Nov 2025 11:34:37 +0000 Subject: [PATCH 15/38] unit test working --- .../__tests__/autocapture-attribution.test.ts | 54 +++++++++++++++---- .../amplitude/logEventV2/index.ts | 2 +- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts index d4a4fdca185..dfccb1fd64c 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -62,7 +62,13 @@ describe('Amplitude', () => { timestamp, event: 'Test Event', traits: { - 'some-trait-key': 'some-trait-value' + otherTraits: {'some-trait-key': 'some-trait-value'}, + setTraits: { + interests: ['music', 'sports'] // should get sent as normal set + }, + setOnceTraits: { + first_name: "Billybob" // should get sent as normal setOnce + } }, context: { integrations: { @@ -76,22 +82,50 @@ describe('Amplitude', () => { } }, page: { - referrer: 'referrer-from-page-context' + referrer: 'referrer-from-page-context' // should get dropped }, campaign: { - name: 'campaign-name-from-campaign-context', - source: 'campaign-source-from-campaign-context', - medium: 'campaign-medium-from-campaign-context', - term: 'campaign-term-from-campaign-context', - content: 'campaign-content-from-campaign-context' + name: 'campaign-name-from-campaign-context', // should get dropped + source: 'campaign-source-from-campaign-context', // should get dropped + medium: 'campaign-medium-from-campaign-context',// should get dropped + term: 'campaign-term-from-campaign-context',// should get dropped + content: 'campaign-content-from-campaign-context'// should get dropped } } }) - const responses = await testDestination.testAction('logEventV2', { event, useDefaultMappings: true }) + const responses = await testDestination.testAction( + 'logEventV2', + { + event, + useDefaultMappings: true, + mapping: { + user_properties: { '@path': '$.traits.otherTraits' }, + setOnce: { + initial_referrer: { '@path': '$.context.page.referrer' }, + initial_utm_source: { '@path': '$.context.campaign.source' }, + initial_utm_medium: { '@path': '$.context.campaign.medium' }, + initial_utm_campaign: { '@path': '$.context.campaign.name' }, + initial_utm_term: { '@path': '$.context.campaign.term' }, + initial_utm_content: { '@path': '$.context.campaign.content' }, + first_name: { '@path': '$.traits.setOnceTraits.first_name' } + }, + setAlways: { + referrer: { '@path': '$.context.page.referrer' }, + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' }, + interests: { '@path': '$.traits.setTraits.interests' } + } + } + } + ) + expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) - expect(responses[0].options.json).toMatchObject({ + expect(responses[0].options.json).toEqual({ api_key: undefined, events: [ { @@ -104,6 +138,7 @@ describe('Amplitude', () => { user_id: "user1234", user_properties: { $set: { + interests: ["music", "sports"], // carried over from the setAlways mapping gclid: "gclid-from-integrations-object", referrer: "referrer-from-integrations-object", referring_domain: "referring-domain-from-integrations-object", @@ -113,6 +148,7 @@ describe('Amplitude', () => { utm_term: "utm-term-from-integrations-object", }, $setOnce: { + first_name: "Billybob", // carried over from the setOnce mapping initial_dclid: "", initial_fbclid: "", initial_gbraid: "", diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index bf8aa718b47..dbd94434c62 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -265,7 +265,7 @@ const action: ActionDefinition = { } = omit(payload, revenueKeys) const properties = rest as AmplitudeEvent let options - +console.log(JSON.stringify(payload, null, 2)) if (properties.platform) { properties.platform = properties.platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } From 9d0e7088e311cb0f1b68c1394d525f1fcc763933 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 12:26:55 +0000 Subject: [PATCH 16/38] refactor --- .../amplitude/autocaptueAttributions.ts | 1 - .../amplitude/autocapture-attribution.ts | 58 +++++++++++++ .../amplitude/autocapture-fields.ts | 42 +++++++++ .../src/destinations/amplitude/compact.ts | 14 --- .../amplitude/logEventV2/generated-types.ts | 6 +- .../amplitude/logEventV2/index.ts | 85 ++----------------- 6 files changed, 112 insertions(+), 94 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/compact.ts diff --git a/packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts b/packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts deleted file mode 100644 index a1a2643db79..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/autocaptueAttributions.ts +++ /dev/null @@ -1 +0,0 @@ -export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts new file mode 100644 index 00000000000..f182debbc5f --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts @@ -0,0 +1,58 @@ +import { Payload } from './logEventV2/generated-types' +import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' + +export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' + +function compact(object: { [k: string]: unknown } | undefined): boolean { + return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 +} + +export function getUserProperties(payload: Payload): { [k: string]: unknown } { + const { + setOnce, + setAlways, + add, + autocaptureAttributionEnabled, + autocaptureAttributionSet, + autocaptureAttributionSetOnce, + autocaptureAttributionUnset, + user_properties + } = payload + + if (autocaptureAttributionEnabled) { + // If autocapture attribution is enabled, we need to make sure that attribution keys are not sent from the setAlways and setOnce fields + for (const key of AMPLITUDE_ATTRIBUTION_KEYS) { + if( typeof setAlways === "object" && setAlways !== null){ + delete setAlways[key] + } + if(typeof setOnce === "object" && setOnce !== null){ + delete setOnce[`initial_${key}`] + } + } + } + + const userProperties = { + ...user_properties, + ...(compact(autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } : setOnce) + ? { $setOnce: autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } : setOnce } + : {}), + ...(compact(autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } : setAlways) + ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } : setAlways } + : {}), + ...(compact(add) ? { $add: add } : {}), + ...(autocaptureAttributionUnset && autocaptureAttributionUnset.length > 0 + ? { $unset: autocaptureAttributionUnset } + : {}) + } + + return userProperties +} + + + + + + + + + \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts new file mode 100644 index 00000000000..5d398137750 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts @@ -0,0 +1,42 @@ +import type { InputField } from '@segment/actions-core' +import { DESTINATION_INTEGRATION_NAME } from './autocapture-attribution' + +export const autocaptureFields: Record = { + add: { + label: 'Add', + description: + "Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0.", + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue' + }, + autocaptureAttributionEnabled: { + label: 'Autocapture Attribution Enabled', + description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', + type: 'boolean', + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, + readOnly: true + }, + autocaptureAttributionSet: { + label: 'Autocapture Attribution Set', + description: 'Utility field used to detect if any attribution values need to be set.', + type: 'object', + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, + readOnly: true + }, + autocaptureAttributionSetOnce: { + label: 'Autocapture Attribution Set Once', + description: 'Utility field used to detect if any attribution values need to be set_once.', + type: 'object', + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, + readOnly: true + }, + autocaptureAttributionUnset: { + label: 'Autocapture Attribution Unset', + description: 'Utility field used to detect if any attribution values need to be unset.', + type: 'string', + multiple: true, + default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, + readOnly: true + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/compact.ts b/packages/destination-actions/src/destinations/amplitude/compact.ts deleted file mode 100644 index 34186e5c573..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/compact.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Payload as LogV2Payload } from './logEventV2/generated-types' - -/** - * Takes an object and removes all keys with a "falsey" value. Then, checks if the object is empty or not. - * - * @param object the setAlways, setOnce, or add object from the LogEvent payload - * @returns a boolean signifying whether the resulting object is empty or not - */ - -export default function compact( - object: LogV2Payload['setOnce'] | LogV2Payload['setAlways'] | LogV2Payload['add'] -): boolean { - return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 -} diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 457d3bac7ff..79768730e94 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -215,19 +215,19 @@ export interface Payload { */ autocaptureAttributionEnabled?: boolean /** - * Utility field which is used to funnel autocaptured attributon data which will be set in Amplitude. + * Utility field used to detect if any attribution values need to be set. */ autocaptureAttributionSet?: { [k: string]: unknown } /** - * Utility field which is used to funnel autocaptured attributon data which will be set_once in Amplitude. + * Utility field used to detect if any attribution values need to be set_once. */ autocaptureAttributionSetOnce?: { [k: string]: unknown } /** - * Utility field which is used to funnel autocaptured attributon data which will be unset in Amplitude. + * Utility field used to detect if any attribution values need to be unset. */ autocaptureAttributionUnset?: string[] /** diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index dbd94434c62..56eda7a630d 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,6 +1,6 @@ import { ActionDefinition, omit, removeUndefined } from '@segment/actions-core' import dayjs from 'dayjs' -import compact from '../compact' + import { eventSchema } from '../event-schema' import type { Settings } from '../generated-types' import { getEndpointByRegion } from '../regional-endpoints' @@ -8,8 +8,8 @@ import { parseUserAgentProperties } from '../user-agent' import type { Payload } from './generated-types' import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' -import { DESTINATION_INTEGRATION_NAME } from '../autocaptueAttributions' -import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' +import { autocaptureFields } from '../autocapture-fields' +import { getUserProperties } from '../autocapture-attribution' export interface AmplitudeEvent extends Omit { library?: string @@ -21,7 +21,8 @@ export interface AmplitudeEvent extends Omit = { title: 'Log Event V2', description: 'Send an event to Amplitude', @@ -169,43 +170,7 @@ const action: ActionDefinition = { utm_content: { '@path': '$.context.campaign.content' } } }, - add: { - label: 'Add', - description: - "Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0.", - type: 'object', - additionalProperties: true, - defaultObjectUI: 'keyvalue' - }, - autocaptureAttributionEnabled: { - label: 'Autocapture Attribution Enabled', - description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', - type: 'boolean', - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, - readOnly: true - }, - autocaptureAttributionSet: { - label: 'Autocapture Attribution Set', - description: 'Utility field used to detect if any attribution values need to be set.', - type: 'object', - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, - readOnly: true - }, - autocaptureAttributionSetOnce: { - label: 'Autocapture Attribution Set Once', - description: 'Utility field used to detect if any attribution values need to be set_once.', - type: 'object', - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, - readOnly: true - }, - autocaptureAttributionUnset: { - label: 'Autocapture Attribution Unset', - description: 'Utility field used to detect if any attribution values need to be unset.', - type: 'string', - multiple: true, - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, - readOnly: true - }, + ...autocaptureFields, use_batch_endpoint: { label: 'Use Batch Endpoint', description: @@ -254,18 +219,11 @@ const action: ActionDefinition = { userAgentData, min_id_length, library, - setOnce, - setAlways, - add, - autocaptureAttributionEnabled, - autocaptureAttributionSet, - autocaptureAttributionSetOnce, - autocaptureAttributionUnset, ...rest - } = omit(payload, revenueKeys) + } = omit(payload, keysToOmit) const properties = rest as AmplitudeEvent let options -console.log(JSON.stringify(payload, null, 2)) + if (properties.platform) { properties.platform = properties.platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } @@ -286,32 +244,7 @@ console.log(JSON.stringify(payload, null, 2)) options = { min_id_length } } - const setUserProperties = ( - name: '$setOnce' | '$set' | '$add' | '$unset', - obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] - ) => { - console.log(name, obj) - if (compact(obj)) { - properties.user_properties = { ...properties.user_properties, [name]: obj } - } - } - - if (autocaptureAttributionEnabled) { - // If autocapture attribution is enabled, we need to make sure that attribution keys are not sent from the setAlways and setOnce fields - for (const key of AMPLITUDE_ATTRIBUTION_KEYS) { - if( typeof setAlways === "object" && setAlways !== null){ - delete setAlways[key] - } - if(typeof setOnce === "object" && setOnce !== null){ - delete setOnce[`initial_${key}`] - } - } - } - - setUserProperties('$setOnce', autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce} : setOnce) - setUserProperties('$set', autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet} : setAlways) - setUserProperties('$add', add) - properties.user_properties = { ...properties.user_properties, ['$unset']: autocaptureAttributionUnset || []} + properties.user_properties = getUserProperties(payload) const events: AmplitudeEvent[] = [ { From 8b3faded7578bb69f7c3a0c026299704682ac539 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 12:32:07 +0000 Subject: [PATCH 17/38] refactor --- .../amplitude/autocapture-fields.ts | 8 ---- .../amplitude/logEventV2/generated-types.ts | 40 +++++++++---------- .../amplitude/logEventV2/index.ts | 10 ++++- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts index 5d398137750..70a8d52c0db 100644 --- a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts @@ -2,14 +2,6 @@ import type { InputField } from '@segment/actions-core' import { DESTINATION_INTEGRATION_NAME } from './autocapture-attribution' export const autocaptureFields: Record = { - add: { - label: 'Add', - description: - "Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0.", - type: 'object', - additionalProperties: true, - defaultObjectUI: 'keyvalue' - }, autocaptureAttributionEnabled: { label: 'Autocapture Attribution Enabled', description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 79768730e94..ed9d16e2893 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -177,6 +177,26 @@ export interface Payload { revenueType?: string [k: string]: unknown }[] + /** + * Utility field used to detect if Autocapture Attribution Plugin is enabled. + */ + autocaptureAttributionEnabled?: boolean + /** + * Utility field used to detect if any attribution values need to be set. + */ + autocaptureAttributionSet?: { + [k: string]: unknown + } + /** + * Utility field used to detect if any attribution values need to be set_once. + */ + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } + /** + * Utility field used to detect if any attribution values need to be unset. + */ + autocaptureAttributionUnset?: string[] /** * The following fields will only be set as user properties if they do not already have a value. */ @@ -210,26 +230,6 @@ export interface Payload { add?: { [k: string]: unknown } - /** - * Utility field used to detect if Autocapture Attribution Plugin is enabled. - */ - autocaptureAttributionEnabled?: boolean - /** - * Utility field used to detect if any attribution values need to be set. - */ - autocaptureAttributionSet?: { - [k: string]: unknown - } - /** - * Utility field used to detect if any attribution values need to be set_once. - */ - autocaptureAttributionSetOnce?: { - [k: string]: unknown - } - /** - * Utility field used to detect if any attribution values need to be unset. - */ - autocaptureAttributionUnset?: string[] /** * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 56eda7a630d..c885086959d 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -89,6 +89,7 @@ const action: ActionDefinition = { ] } }, + ...autocaptureFields, setOnce: { label: 'Set Once', description: 'The following fields will only be set as user properties if they do not already have a value.', @@ -170,7 +171,14 @@ const action: ActionDefinition = { utm_content: { '@path': '$.context.campaign.content' } } }, - ...autocaptureFields, + add: { + label: 'Add', + description: + "Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0.", + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue' + }, use_batch_endpoint: { label: 'Use Batch Endpoint', description: From d7ad4b13149806fb5586bb18053582da862bd27e Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 12:47:50 +0000 Subject: [PATCH 18/38] refactor --- .../amplitude-plugins/src/autocaptureAttribution/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index 7a718da8854..f82cb697fb5 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -10,9 +10,9 @@ import isEqual from 'lodash/isEqual' const action: BrowserActionDefinition = { title: 'Autocapture Attribution Plugin', - description: 'Captures attribution details from the URL and attaches them to every Amplitude browser based event.', + description: 'Captures attribution details from the URL and attaches them to every Amplitude browser based event. Use with the Log Event V2 action to automate the collection of attribution data.', platform: 'web', - defaultSubscription: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', + defaultSubscription: 'type = "track"', fields: { excludeReferrers: { label: 'Exclude Referrers', From 6be8bc82571f2e67e5da06b0d79b79032c460dca Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 13:02:18 +0000 Subject: [PATCH 19/38] moving some files around --- packages/actions-shared/src/amplitude/types.ts | 2 +- .../amplitude/{ => logEventV2}/autocapture-attribution.ts | 2 +- .../amplitude/{ => logEventV2}/autocapture-fields.ts | 0 .../src/destinations/amplitude/logEventV2/index.ts | 5 ++--- 4 files changed, 4 insertions(+), 5 deletions(-) rename packages/destination-actions/src/destinations/amplitude/{ => logEventV2}/autocapture-attribution.ts (97%) rename packages/destination-actions/src/destinations/amplitude/{ => logEventV2}/autocapture-fields.ts (100%) diff --git a/packages/actions-shared/src/amplitude/types.ts b/packages/actions-shared/src/amplitude/types.ts index a7b5d50a69a..9f5ce56c932 100644 --- a/packages/actions-shared/src/amplitude/types.ts +++ b/packages/actions-shared/src/amplitude/types.ts @@ -1,4 +1,4 @@ -import { AMPLITUDE_ATTRIBUTION_KEYS } from '../amplitude/constants' +import { AMPLITUDE_ATTRIBUTION_KEYS } from './constants' export type AmplitudeAttributionKey = typeof AMPLITUDE_ATTRIBUTION_KEYS[number] diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts similarity index 97% rename from packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts rename to packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts index f182debbc5f..c21e258c4ba 100644 --- a/packages/destination-actions/src/destinations/amplitude/autocapture-attribution.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts @@ -1,5 +1,5 @@ -import { Payload } from './logEventV2/generated-types' import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' +import { Payload } from './generated-types' export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts similarity index 100% rename from packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts rename to packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index c885086959d..9318044598d 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,6 +1,5 @@ import { ActionDefinition, omit, removeUndefined } from '@segment/actions-core' import dayjs from 'dayjs' - import { eventSchema } from '../event-schema' import type { Settings } from '../generated-types' import { getEndpointByRegion } from '../regional-endpoints' @@ -8,8 +7,8 @@ import { parseUserAgentProperties } from '../user-agent' import type { Payload } from './generated-types' import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' -import { autocaptureFields } from '../autocapture-fields' -import { getUserProperties } from '../autocapture-attribution' +import { autocaptureFields } from './autocapture-fields' +import { getUserProperties } from './autocapture-attribution' export interface AmplitudeEvent extends Omit { library?: string From 831baba8689a812e7907efb6752395d9932f35f6 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 13:25:55 +0000 Subject: [PATCH 20/38] more refactoring --- .../__tests__/autocapture-attribution.test.ts | 204 ++++++++++++++++++ .../amplitude/logEventV2/generated-types.ts | 4 +- .../amplitude/logEventV2/index.ts | 4 +- 3 files changed, 208 insertions(+), 4 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts index dfccb1fd64c..5c09862dcd8 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -190,5 +190,209 @@ describe('Amplitude', () => { options: undefined, }) }) + + it('Blocks utm and referrer data if autocapture attribution is enabled', async () => { + nock('https://api2.amplitude.com/2').post('/httpapi').reply(200, {}) + + const event = createTestEvent({ + timestamp, + event: 'Test Event', + traits: { + otherTraits: {'some-trait-key': 'some-trait-value'}, + setTraits: { + interests: ['music', 'sports'] // should get sent as normal set + }, + setOnceTraits: { + first_name: "Billybob" // should get sent as normal setOnce + } + }, + context: { + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + enabled: true, + set_once: {}, // no attribution values provided - should still block mapped values + set: {}, + unset: [] + } + } + }, + page: { + referrer: 'referrer-from-page-context' // should get ignored + }, + campaign: { + name: 'campaign-name-from-campaign-context', // should get ignored + source: 'campaign-source-from-campaign-context', // should get ignored + medium: 'campaign-medium-from-campaign-context',// should get ignored + term: 'campaign-term-from-campaign-context',// should get ignored + content: 'campaign-content-from-campaign-context'// should get ignored + } + } + }) + + const responses = await testDestination.testAction( + 'logEventV2', + { + event, + useDefaultMappings: true, + mapping: { + user_properties: { '@path': '$.traits.otherTraits' }, + setOnce: { + initial_referrer: { '@path': '$.context.page.referrer' }, + initial_utm_source: { '@path': '$.context.campaign.source' }, + initial_utm_medium: { '@path': '$.context.campaign.medium' }, + initial_utm_campaign: { '@path': '$.context.campaign.name' }, + initial_utm_term: { '@path': '$.context.campaign.term' }, + initial_utm_content: { '@path': '$.context.campaign.content' }, + first_name: { '@path': '$.traits.setOnceTraits.first_name' } + }, + setAlways: { + referrer: { '@path': '$.context.page.referrer' }, + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' }, + interests: { '@path': '$.traits.setTraits.interests' } + } + } + } + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toEqual({ + api_key: undefined, + events: [ + { + device_id: "anonId1234", + event_properties: {}, + event_type: "Test Event", + library: "segment", + time: 1629213675449, + use_batch_endpoint: false, + user_id: "user1234", + user_properties: { + $set: { + interests: ["music", "sports"], // carried over from the setAlways mapping + }, + $setOnce: { + first_name: "Billybob", // carried over from the setOnce mapping + }, + "some-trait-key": "some-trait-value", + }, + }, + ], + options: undefined, + }) + }) + + it('regular utm and referrer data is sent when autocapture attribution is disabled', async () => { + nock('https://api2.amplitude.com/2').post('/httpapi').reply(200, {}) + + const event = createTestEvent({ + timestamp, + event: 'Test Event', + traits: { + otherTraits: {'some-trait-key': 'some-trait-value'}, + setTraits: { + interests: ['music', 'sports'] // should get sent as normal set + }, + setOnceTraits: { + first_name: "Billybob" // should get sent as normal setOnce + } + }, + context: { + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + // enabled: true, // Disabled autocapture attribution + set_once: {}, + set: {}, + unset: [] + } + } + }, + page: { + referrer: 'referrer-from-page-context' // should get handled normally + }, + campaign: { + name: 'campaign-name-from-campaign-context', // should get handled normally + source: 'campaign-source-from-campaign-context', // should get handled normally + medium: 'campaign-medium-from-campaign-context',// should get handled normally + term: 'campaign-term-from-campaign-context',// should get handled normally + content: 'campaign-content-from-campaign-context'// should get handled normally + } + } + }) + + const responses = await testDestination.testAction( + 'logEventV2', + { + event, + useDefaultMappings: true, + mapping: { + user_properties: { '@path': '$.traits.otherTraits' }, + setOnce: { + initial_referrer: { '@path': '$.context.page.referrer' }, + initial_utm_source: { '@path': '$.context.campaign.source' }, + initial_utm_medium: { '@path': '$.context.campaign.medium' }, + initial_utm_campaign: { '@path': '$.context.campaign.name' }, + initial_utm_term: { '@path': '$.context.campaign.term' }, + initial_utm_content: { '@path': '$.context.campaign.content' }, + first_name: { '@path': '$.traits.setOnceTraits.first_name' } + }, + setAlways: { + referrer: { '@path': '$.context.page.referrer' }, + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' }, + interests: { '@path': '$.traits.setTraits.interests' } + } + } + } + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toEqual({ + api_key: undefined, + events: [ + { + device_id: "anonId1234", + event_properties: {}, + event_type: "Test Event", + library: "segment", + time: 1629213675449, + use_batch_endpoint: false, + user_id: "user1234", + user_properties: { + $set: { + interests: ["music", "sports"], + referrer: "referrer-from-page-context", + utm_campaign: "campaign-name-from-campaign-context", + utm_content: "campaign-content-from-campaign-context", + utm_medium: "campaign-medium-from-campaign-context", + utm_source: "campaign-source-from-campaign-context", + utm_term: "campaign-term-from-campaign-context" + }, + $setOnce: { + first_name: "Billybob", + initial_referrer: "referrer-from-page-context", + initial_utm_campaign: "campaign-name-from-campaign-context", + initial_utm_content: "campaign-content-from-campaign-context", + initial_utm_medium: "campaign-medium-from-campaign-context", + initial_utm_source: "campaign-source-from-campaign-context", + initial_utm_term: "campaign-term-from-campaign-context" + }, + "some-trait-key": "some-trait-value" + } + } + ], + options: undefined + }) + }) }) }) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index ed9d16e2893..56f342c51d4 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -198,7 +198,7 @@ export interface Payload { */ autocaptureAttributionUnset?: string[] /** - * The following fields will only be set as user properties if they do not already have a value. + * The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. */ setOnce?: { /** @@ -213,7 +213,7 @@ export interface Payload { [k: string]: unknown } /** - * The following fields will be set as user properties for every event. + * The following fields will be set as user properties for every event. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. */ setAlways?: { referrer?: string diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 9318044598d..e05273db7cf 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -91,7 +91,7 @@ const action: ActionDefinition = { ...autocaptureFields, setOnce: { label: 'Set Once', - description: 'The following fields will only be set as user properties if they do not already have a value.', + description: "The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored.", type: 'object', additionalProperties: true, properties: { @@ -132,7 +132,7 @@ const action: ActionDefinition = { }, setAlways: { label: 'Set Always', - description: 'The following fields will be set as user properties for every event.', + description: "The following fields will be set as user properties for every event. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored.", type: 'object', additionalProperties: true, properties: { From 0a64fd34350314d6b6fcd6e336f82ed02cb309ba Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 14:09:56 +0000 Subject: [PATCH 21/38] fixing integrations path --- .../__tests__/autocapture-attribution.test.ts | 61 ++++++++++--------- .../logEventV2/autocapture-fields.ts | 8 +-- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts index 5c09862dcd8..ceda377f32f 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -70,17 +70,18 @@ describe('Amplitude', () => { first_name: "Billybob" // should get sent as normal setOnce } }, - context: { - integrations: { - 'Actions Amplitude': { - autocapture_attribution: { - enabled: true, - set_once, - set, - unset - } + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + enabled: true, + set_once, + set, + unset } - }, + } + }, + context: { + page: { referrer: 'referrer-from-page-context' // should get dropped }, @@ -206,17 +207,17 @@ describe('Amplitude', () => { first_name: "Billybob" // should get sent as normal setOnce } }, - context: { - integrations: { - 'Actions Amplitude': { - autocapture_attribution: { - enabled: true, - set_once: {}, // no attribution values provided - should still block mapped values - set: {}, - unset: [] - } + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + enabled: true, + set_once: {}, // no attribution values provided - should still block mapped values + set: {}, + unset: [] } - }, + } + }, + context: { page: { referrer: 'referrer-from-page-context' // should get ignored }, @@ -302,17 +303,17 @@ describe('Amplitude', () => { first_name: "Billybob" // should get sent as normal setOnce } }, - context: { - integrations: { - 'Actions Amplitude': { - autocapture_attribution: { - // enabled: true, // Disabled autocapture attribution - set_once: {}, - set: {}, - unset: [] - } + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + // enabled: true, // Disabled autocapture attribution + set_once: {}, + set: {}, + unset: [] } - }, + } + }, + context: { page: { referrer: 'referrer-from-page-context' // should get handled normally }, diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts index 70a8d52c0db..3807eb8d414 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts @@ -6,21 +6,21 @@ export const autocaptureFields: Record = { label: 'Autocapture Attribution Enabled', description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', type: 'boolean', - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, readOnly: true }, autocaptureAttributionSet: { label: 'Autocapture Attribution Set', description: 'Utility field used to detect if any attribution values need to be set.', type: 'object', - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, readOnly: true }, autocaptureAttributionSetOnce: { label: 'Autocapture Attribution Set Once', description: 'Utility field used to detect if any attribution values need to be set_once.', type: 'object', - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, readOnly: true }, autocaptureAttributionUnset: { @@ -28,7 +28,7 @@ export const autocaptureFields: Record = { description: 'Utility field used to detect if any attribution values need to be unset.', type: 'string', multiple: true, - default: { '@path': `$.context.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, readOnly: true } } \ No newline at end of file From 2c008608423557e49ff18b247177757cc1d18622 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 14:24:40 +0000 Subject: [PATCH 22/38] referrer unit test --- .../__tests__/index.test.ts | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index 1e3e9a24e11..213b6d1995c 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -231,7 +231,7 @@ describe('ajs-integration', () => { ) /* - * Finally we test with a completely new attribution parameter + * Next we test with a completely new attribution parameter */ Object.defineProperty(window, 'location', { value: { @@ -295,5 +295,77 @@ describe('ajs-integration', () => { } ) + /* + * Next we test with a completely some attributes we've never seen before, referrer and referring_domain + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://blah.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) + + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://blah.com/path/page/hello/', + }) + + const updatedCtx6 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj6 = updatedCtx6?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj6).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + referrer: "https://blah.com/path/page/hello/", + referring_domain: "blah.com" + }, + set_once: { + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer: "https://blah.com/path/page/hello/", + initial_referring_domain: "blah.com", + initial_rtd_cid: "", + initial_ttclid: "", + initial_twclid: "", + initial_utm_campaign: "", + initial_utm_content: "", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "", + initial_utm_term: "", + initial_wbraid: "" + }, + unset: [ + "utm_source", + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "utm_id", + "dclid", + "fbclid", + "gbraid", + "wbraid", + "gclid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] + } + } + ) + }) }) \ No newline at end of file From c511b3156433c840769180f6e31359307b1fec1e Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 14 Nov 2025 16:48:33 +0000 Subject: [PATCH 23/38] more front end unit tests --- .../__tests__/index.test.ts | 701 ++++++++++-------- 1 file changed, 401 insertions(+), 300 deletions(-) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index 213b6d1995c..ccfe282eadd 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -3,173 +3,97 @@ import { Subscription } from '@segment/browser-destination-runtime/types' import browserPluginsDestination from '../../' import { DESTINATION_INTEGRATION_NAME } from '../../constants' -const example: Subscription[] = [ - { - partnerAction: 'autocaptureAttribution', - name: 'Autocapture Attribution Plugin', - enabled: true, - subscribe: 'type = "track"', - mapping: {} - } -] - -let browserActions: Plugin[] -let autocaptureAttributionPlugin: Plugin -let ajs: Analytics - -beforeAll(async () => { - browserActions = await browserPluginsDestination({ subscriptions: example }) - autocaptureAttributionPlugin = browserActions[0] - - ajs = new Analytics({ - writeKey: 'w_123' - }) - - // window.localStorage.clear() - - Object.defineProperty(window, 'location', { - value: { - search: '?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale&utm_term=running+shoes&utm_content=ad1&gclid=gclid1234&gbraid=gbraid5678' - }, - writable: true - }) -}) - describe('ajs-integration', () => { - test('updates the original event with with attributions values from the URL, caches the values, then updates when new values come along', async () => { - await autocaptureAttributionPlugin.load(Context.system(), ajs) - const ctx = new Context({ - type: 'track', - event: 'Test Event', - properties: { - greeting: 'Yo!' - } - }) - /* - * First event on the page with attribution values will be transmitted with set and set_once values - */ - const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj).toEqual({ - autocapture_attribution: { + describe('autocapture works as expected', () => { + + const example: Subscription[] = [ + { + partnerAction: 'autocaptureAttribution', + name: 'Autocapture Attribution Plugin', enabled: true, - set: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - }, - set_once: { - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "gbraid5678", - initial_gclid: "gclid1234", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", - initial_referrer: "", - initial_referring_domain: "", - initial_rtd_cid: "", - initial_ttclid: "", - initial_twclid: "", - initial_utm_campaign: "spring_sale", - initial_utm_content: "ad1", - initial_utm_id: "", - initial_utm_medium: "cpc", - initial_utm_source: "google", - initial_utm_term: "running shoes", - initial_wbraid: "" - }, - unset: [ - "referrer", - "referring_domain", - "utm_id", - "dclid", - "fbclid", - "wbraid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid" - ] + subscribe: 'type = "track"', + mapping: {} } - }) + ] - /* - * Second event on the same page with attribution values will be transmitted without set and set_once values - */ - const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj1).toEqual({ - autocapture_attribution: { - enabled: true, - set_once: {}, - set: {}, - unset: [] - } - }) + let browserActions: Plugin[] + let autocaptureAttributionPlugin: Plugin + let ajs: Analytics + + beforeAll(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + autocaptureAttributionPlugin = browserActions[0] + ajs = new Analytics({ + writeKey: 'w_123' + }) - /* - * A new URL should result in updated set and unset values being sent in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?utm_source=email' - }, - writable: true + // window.localStorage.clear() + + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale&utm_term=running+shoes&utm_content=ad1&gclid=gclid1234&gbraid=gbraid5678' + }, + writable: true + }) }) - const updatedCtx2 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj2 = updatedCtx2?.event?.integrations[DESTINATION_INTEGRATION_NAME] + test('updates the original event with with attributions values from the URL, caches the values, then updates when new values come along', async () => { + await autocaptureAttributionPlugin.load(Context.system(), ajs) + const ctx = new Context({ + type: 'track', + event: 'Test Event', + properties: { + greeting: 'Yo!' + } + }) - expect(ampIntegrationsObj2).toEqual( - { + /* + * First event on the page with attribution values will be transmitted with set and set_once values + */ + const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj).toEqual({ autocapture_attribution: { enabled: true, set: { - utm_source: "email", + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", }, set_once: { initial_dclid: "", initial_fbclid: "", - initial_gbraid: "", - initial_gclid: "", + initial_gbraid: "gbraid5678", + initial_gclid: "gclid1234", initial_ko_clickid: "", initial_li_fat_id: "", initial_msclkid: "", - initial_referrer:"", + initial_referrer: "", initial_referring_domain: "", initial_rtd_cid: "", initial_ttclid: "", initial_twclid: "", - initial_utm_campaign: "", - initial_utm_content: "", + initial_utm_campaign: "spring_sale", + initial_utm_content: "ad1", initial_utm_id: "", - initial_utm_medium: "", - initial_utm_source: "email", - initial_utm_term: "", + initial_utm_medium: "cpc", + initial_utm_source: "google", + initial_utm_term: "running shoes", initial_wbraid: "" }, unset: [ - 'referrer', - 'referring_domain', - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content", + "referrer", + "referring_domain", "utm_id", "dclid", "fbclid", - "gbraid", "wbraid", - "gclid", "ko_clickid", "li_fat_id", "msclkid", @@ -178,194 +102,371 @@ describe('ajs-integration', () => { "twclid" ] } - } - ) - - /* - * Next a new page load happens which does not have any valid attribution values. No attribution values should be sent in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?' - }, - writable: true - }) + }) - const updatedCtx3 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj3 = updatedCtx3?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj3).toEqual( - { + /* + * Second event on the same page with attribution values will be transmitted without set and set_once values + */ + const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj1).toEqual({ autocapture_attribution: { enabled: true, - set: {}, set_once: {}, + set: {}, unset: [] } + }) + + + /* + * A new URL should result in updated set and unset values being sent in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=email' + }, + writable: true + }) + + const updatedCtx2 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj2 = updatedCtx2?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj2).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + utm_source: "email", + }, + set_once: { + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer:"", + initial_referring_domain: "", + initial_rtd_cid: "", + initial_ttclid: "", + initial_twclid: "", + initial_utm_campaign: "", + initial_utm_content: "", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "email", + initial_utm_term: "", + initial_wbraid: "" + }, + unset: [ + 'referrer', + 'referring_domain', + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "utm_id", + "dclid", + "fbclid", + "gbraid", + "wbraid", + "gclid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] + } + } + ) + + /* + * Next a new page load happens which does not have any valid attribution values. No attribution values should be sent in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?' + }, + writable: true + }) + + const updatedCtx3 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj3 = updatedCtx3?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj3).toEqual( + { + autocapture_attribution: { + enabled: true, + set: {}, + set_once: {}, + unset: [] + } + } + ) + + + /* + * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?some_fake_non_attribution_param=12345' + }, + writable: true + }) + + const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj4).toEqual( + { + autocapture_attribution: { + enabled: true, + set: {}, + set_once: {}, + unset: [] + } + } + ) + + /* + * Next we test with a completely new attribution parameter + */ + Object.defineProperty(window, 'location', { + value: { + search: '?ttclid=uyiuyiuy' + }, + writable: true + }) + + const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj5).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + ttclid: "uyiuyiuy" + }, + set_once: { + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer: "", + initial_referring_domain: "", + initial_rtd_cid: "", + initial_ttclid: "uyiuyiuy", + initial_twclid: "", + initial_utm_campaign: "", + initial_utm_content: "", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "", + initial_utm_term: "", + initial_wbraid: "" + }, + unset: [ + "referrer", + "referring_domain", + "utm_source", + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "utm_id", + "dclid", + "fbclid", + "gbraid", + "wbraid", + "gclid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "twclid" + ] + } + } + ) + + /* + * Next we test with a some attributes we've never seen before, referrer and referring_domain + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://blah.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) + + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://blah.com/path/page/hello/', + }) + + const updatedCtx6 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj6 = updatedCtx6?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj6).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + referrer: "https://blah.com/path/page/hello/", + referring_domain: "blah.com" + }, + set_once: { + initial_dclid: "", + initial_fbclid: "", + initial_gbraid: "", + initial_gclid: "", + initial_ko_clickid: "", + initial_li_fat_id: "", + initial_msclkid: "", + initial_referrer: "https://blah.com/path/page/hello/", + initial_referring_domain: "blah.com", + initial_rtd_cid: "", + initial_ttclid: "", + initial_twclid: "", + initial_utm_campaign: "", + initial_utm_content: "", + initial_utm_id: "", + initial_utm_medium: "", + initial_utm_source: "", + initial_utm_term: "", + initial_wbraid: "" + }, + unset: [ + "utm_source", + "utm_medium", + "utm_campaign", + "utm_term", + "utm_content", + "utm_id", + "dclid", + "fbclid", + "gbraid", + "wbraid", + "gclid", + "ko_clickid", + "li_fat_id", + "msclkid", + "rtd_cid", + "ttclid", + "twclid" + ] + } + } + ) + }) + }) + + describe('autocapture can be blocked as expected', () => { + + const example: Subscription[] = [ + { + partnerAction: 'autocaptureAttribution', + name: 'Autocapture Attribution Plugin', + enabled: true, + subscribe: 'type = "track"', + mapping: { + excludeReferrers: ["test.com", "sub.blah.com", "meh.blah.com"] + } } - ) + ] + let browserActions: Plugin[] + let autocaptureAttributionPlugin: Plugin + let ajs: Analytics - /* - * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?some_fake_non_attribution_param=12345' - }, - writable: true + beforeAll(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + autocaptureAttributionPlugin = browserActions[0] + + ajs = new Analytics({ + writeKey: 'w_123' + }) }) - const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] + test('should prevent attribution from blocked domain', async () => { + await autocaptureAttributionPlugin.load(Context.system(), ajs) + const ctx = new Context({ + type: 'track', + event: 'Test Event', + properties: { + greeting: 'Yo!' + } + }) - expect(ampIntegrationsObj4).toEqual( - { + /* + * Page referrer is in the exclude list - no autocaptured attribution values should be sent. + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://test.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) + + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://test.com/path/page/hello/', + }) + + const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj).toEqual({ autocapture_attribution: { enabled: true, set: {}, set_once: {}, unset: [] } - } - ) - - /* - * Next we test with a completely new attribution parameter - */ - Object.defineProperty(window, 'location', { - value: { - search: '?ttclid=uyiuyiuy' - }, - writable: true - }) + }) - const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] + /* + * Same test as before but this time with a sub domain + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://sub.blah.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) - expect(ampIntegrationsObj5).toEqual( - { - autocapture_attribution: { + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://sub.blah.com/path/page/hello/', + }) + + const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj1).toEqual({ + autocapture_attribution: { enabled: true, - set: { - ttclid: "uyiuyiuy" - }, - set_once: { - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "", - initial_gclid: "", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", - initial_referrer: "", - initial_referring_domain: "", - initial_rtd_cid: "", - initial_ttclid: "uyiuyiuy", - initial_twclid: "", - initial_utm_campaign: "", - initial_utm_content: "", - initial_utm_id: "", - initial_utm_medium: "", - initial_utm_source: "", - initial_utm_term: "", - initial_wbraid: "" - }, - unset: [ - "referrer", - "referring_domain", - "utm_source", - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content", - "utm_id", - "dclid", - "fbclid", - "gbraid", - "wbraid", - "gclid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "twclid" - ] + set: {}, + set_once: {}, + unset: [] } - } - ) - - /* - * Next we test with a completely some attributes we've never seen before, referrer and referring_domain - */ - Object.defineProperty(window, 'location', { - value: { - href: 'https://blah.com/path/page/hello/', - search: '', - protocol: 'https:' - }, - writable: true - }) + }) - Object.defineProperty(document, 'referrer', { - writable: true, - value: 'https://blah.com/path/page/hello/', }) + }) - const updatedCtx6 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj6 = updatedCtx6?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj6).toEqual( - { - autocapture_attribution: { - enabled: true, - set: { - referrer: "https://blah.com/path/page/hello/", - referring_domain: "blah.com" - }, - set_once: { - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "", - initial_gclid: "", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", - initial_referrer: "https://blah.com/path/page/hello/", - initial_referring_domain: "blah.com", - initial_rtd_cid: "", - initial_ttclid: "", - initial_twclid: "", - initial_utm_campaign: "", - initial_utm_content: "", - initial_utm_id: "", - initial_utm_medium: "", - initial_utm_source: "", - initial_utm_term: "", - initial_wbraid: "" - }, - unset: [ - "utm_source", - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content", - "utm_id", - "dclid", - "fbclid", - "gbraid", - "wbraid", - "gclid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid" - ] - } - } - ) +}) - }) -}) \ No newline at end of file From 21971eb1b942d22c5503de9d329399ba55797e57 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 11:34:38 +0000 Subject: [PATCH 24/38] making updates after meeting with Amplitude --- .../actions-shared/src/amplitude/types.ts | 4 +- .../__tests__/index.test.ts | 289 +++++++++--------- .../src/autocaptureAttribution/index.ts | 8 +- .../__tests__/autocapture-attribution.test.ts | 114 +++---- .../logEventV2/autocapture-attribution.ts | 4 +- .../logEventV2/autocapture-fields.ts | 3 +- .../amplitude/logEventV2/generated-types.ts | 4 +- 7 files changed, 212 insertions(+), 214 deletions(-) diff --git a/packages/actions-shared/src/amplitude/types.ts b/packages/actions-shared/src/amplitude/types.ts index 9f5ce56c932..61fe58c3b65 100644 --- a/packages/actions-shared/src/amplitude/types.ts +++ b/packages/actions-shared/src/amplitude/types.ts @@ -6,4 +6,6 @@ export type AmplitudeSetOnceAttributionKey = `initial_${AmplitudeAttributionKey} export type AmplitudeAttributionValues = Record -export type AmplitudeSetOnceAttributionValues = Record \ No newline at end of file +export type AmplitudeAttributionUnsetValues = Record + +export type AmplitudeSetOnceAttributionValues = Record \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts index ccfe282eadd..e15618825de 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts @@ -4,7 +4,6 @@ import browserPluginsDestination from '../../' import { DESTINATION_INTEGRATION_NAME } from '../../constants' describe('ajs-integration', () => { - describe('autocapture works as expected', () => { const example: Subscription[] = [ @@ -67,40 +66,40 @@ describe('ajs-integration', () => { utm_term: "running shoes", }, set_once: { - initial_dclid: "", - initial_fbclid: "", + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", initial_gbraid: "gbraid5678", initial_gclid: "gclid1234", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", - initial_referrer: "", - initial_referring_domain: "", - initial_rtd_cid: "", - initial_ttclid: "", - initial_twclid: "", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer: "EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", initial_utm_campaign: "spring_sale", initial_utm_content: "ad1", - initial_utm_id: "", + initial_utm_id: "EMPTY", initial_utm_medium: "cpc", initial_utm_source: "google", initial_utm_term: "running shoes", - initial_wbraid: "" + initial_wbraid: "EMPTY" }, - unset: [ - "referrer", - "referring_domain", - "utm_id", - "dclid", - "fbclid", - "wbraid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid" - ] + unset: { + referrer: "-", + referring_domain: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + wbraid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-" + } } }) @@ -114,7 +113,7 @@ describe('ajs-integration', () => { enabled: true, set_once: {}, set: {}, - unset: [] + unset: {} } }) @@ -140,46 +139,46 @@ describe('ajs-integration', () => { utm_source: "email", }, set_once: { - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "", - initial_gclid: "", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", - initial_referrer:"", - initial_referring_domain: "", - initial_rtd_cid: "", - initial_ttclid: "", - initial_twclid: "", - initial_utm_campaign: "", - initial_utm_content: "", - initial_utm_id: "", - initial_utm_medium: "", + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer:"EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", initial_utm_source: "email", - initial_utm_term: "", - initial_wbraid: "" + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" }, - unset: [ - 'referrer', - 'referring_domain', - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content", - "utm_id", - "dclid", - "fbclid", - "gbraid", - "wbraid", - "gclid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid" - ] + unset: { + referrer: "-", + referring_domain: "-", + utm_medium: "-", + utm_campaign: "-", + utm_term: "-", + utm_content: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + gbraid: "-", + wbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-" + } } } ) @@ -203,7 +202,7 @@ describe('ajs-integration', () => { enabled: true, set: {}, set_once: {}, - unset: [] + unset: {} } } ) @@ -228,7 +227,7 @@ describe('ajs-integration', () => { enabled: true, set: {}, set_once: {}, - unset: [] + unset: {} } } ) @@ -254,46 +253,46 @@ describe('ajs-integration', () => { ttclid: "uyiuyiuy" }, set_once: { - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "", - initial_gclid: "", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", - initial_referrer: "", - initial_referring_domain: "", - initial_rtd_cid: "", + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer: "EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", initial_ttclid: "uyiuyiuy", - initial_twclid: "", - initial_utm_campaign: "", - initial_utm_content: "", - initial_utm_id: "", - initial_utm_medium: "", - initial_utm_source: "", - initial_utm_term: "", - initial_wbraid: "" + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", + initial_utm_source: "EMPTY", + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" }, - unset: [ - "referrer", - "referring_domain", - "utm_source", - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content", - "utm_id", - "dclid", - "fbclid", - "gbraid", - "wbraid", - "gclid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "twclid" - ] + unset: { + referrer: "-", + referring_domain: "-", + utm_source: "-", + utm_medium: "-", + utm_campaign: "-", + utm_term: "-", + utm_content: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + gbraid: "-", + wbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + twclid: "-" + } } } ) @@ -327,51 +326,50 @@ describe('ajs-integration', () => { referring_domain: "blah.com" }, set_once: { - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "", - initial_gclid: "", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", initial_referrer: "https://blah.com/path/page/hello/", initial_referring_domain: "blah.com", - initial_rtd_cid: "", - initial_ttclid: "", - initial_twclid: "", - initial_utm_campaign: "", - initial_utm_content: "", - initial_utm_id: "", - initial_utm_medium: "", - initial_utm_source: "", - initial_utm_term: "", - initial_wbraid: "" + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", + initial_utm_source: "EMPTY", + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" }, - unset: [ - "utm_source", - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content", - "utm_id", - "dclid", - "fbclid", - "gbraid", - "wbraid", - "gclid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid" - ] + unset: { + utm_source: "-", + utm_medium: "-", + utm_campaign: "-", + utm_term: "-", + utm_content: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + gbraid: "-", + wbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-" + } } } ) }) }) - describe('autocapture can be blocked as expected', () => { const example: Subscription[] = [ @@ -433,7 +431,7 @@ describe('ajs-integration', () => { enabled: true, set: {}, set_once: {}, - unset: [] + unset: {} } }) @@ -461,12 +459,11 @@ describe('ajs-integration', () => { enabled: true, set: {}, set_once: {}, - unset: [] + unset: {} } }) }) }) - }) diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts index f82cb697fb5..1591efcdbb9 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts @@ -4,7 +4,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' -import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeAttributionKey, AmplitudeSetOnceAttributionValues } from '@segment/actions-shared' +import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeSetOnceAttributionValues, AmplitudeAttributionUnsetValues } from '@segment/actions-shared' import { DESTINATION_INTEGRATION_NAME } from '../constants' import isEqual from 'lodash/isEqual' @@ -32,17 +32,17 @@ const action: BrowserActionDefinition = { const previous = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) const setOnce: Partial = {} const set: Partial = {} - const unset: AmplitudeAttributionKey[] = [] + const unset: Partial = {} const currentPageHasAttribution = current && Object.values(current).some(v => typeof v === 'string' && v.length > 0) if (currentPageHasAttribution && !isEqual(current, previous)){ AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { - setOnce[`initial_${key}`] = current[key] ?? "" + setOnce[`initial_${key}`] = current[key]?.trim() || "EMPTY" if(current[key]){ set[key] = current[key] } else{ - unset.push(key) + unset[key] = '-' } }) if(Object.entries(current).length >0) { diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts index ceda377f32f..90c0f6cbe48 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -1,7 +1,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration } from '@segment/actions-core' import Amplitude from '../index' -import {AmplitudeAttributionValues, AmplitudeSetOnceAttributionValues, AmplitudeAttributionKey} from '@segment/actions-shared' +import {AmplitudeAttributionValues, AmplitudeSetOnceAttributionValues, AmplitudeAttributionUnsetValues} from '@segment/actions-shared' const testDestination = createTestIntegration(Amplitude) const timestamp = '2021-08-17T15:21:15.449Z' @@ -15,22 +15,22 @@ describe('Amplitude', () => { initial_referrer: 'initial-referrer-from-integrations-object', initial_utm_campaign: 'initial-utm-campaign-from-integrations-object', initial_utm_content: 'initial-utm-content-from-integrations-object', - initial_utm_medium: '', + initial_utm_medium: 'EMPTY', initial_utm_source: 'initial-utm-source-from-integrations-object', initial_utm_term: 'initial-utm-term-from-integrations-object', initial_gclid: 'initial-gclid-from-integrations-object', - initial_fbclid: '', - initial_dclid: '', - initial_gbraid: '', - initial_wbraid: '', - initial_ko_clickid: '', - initial_li_fat_id: '', - initial_msclkid: '', + initial_fbclid: 'EMPTY', + initial_dclid: 'EMPTY', + initial_gbraid: 'EMPTY', + initial_wbraid: 'EMPTY', + initial_ko_clickid: 'EMPTY', + initial_li_fat_id: 'EMPTY', + initial_msclkid: 'EMPTY', initial_referring_domain: 'initial-referring-domain-from-integrations-object', - initial_rtd_cid: '', - initial_ttclid: '', - initial_twclid: '', - initial_utm_id: '' + initial_rtd_cid: 'EMPTY', + initial_ttclid: 'EMPTY', + initial_twclid: 'EMPTY', + initial_utm_id: 'EMPTY' } const set: Partial = { @@ -43,20 +43,20 @@ describe('Amplitude', () => { referring_domain: 'referring-domain-from-integrations-object' } - const unset: AmplitudeAttributionKey[] = [ - 'utm_medium', - 'fbclid', - 'dclid', - 'gbraid', - 'wbraid', - 'ko_clickid', - 'li_fat_id', - 'msclkid', - 'rtd_cid', - 'ttclid', - 'twclid', - 'utm_id' - ] + const unset: Partial = { + utm_medium: '-', + fbclid: '-', + dclid: '-', + gbraid: '-', + wbraid: '-', + ko_clickid: '-', + li_fat_id: '-', + msclkid: '-', + rtd_cid: '-', + ttclid: '-', + twclid: '-', + utm_id: '-' + } const event = createTestEvent({ timestamp, @@ -150,40 +150,40 @@ describe('Amplitude', () => { }, $setOnce: { first_name: "Billybob", // carried over from the setOnce mapping - initial_dclid: "", - initial_fbclid: "", - initial_gbraid: "", + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", initial_gclid: "initial-gclid-from-integrations-object", - initial_ko_clickid: "", - initial_li_fat_id: "", - initial_msclkid: "", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", initial_referrer: "initial-referrer-from-integrations-object", initial_referring_domain: "initial-referring-domain-from-integrations-object", - initial_rtd_cid: "", - initial_ttclid: "", - initial_twclid: "", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", initial_utm_campaign: "initial-utm-campaign-from-integrations-object", initial_utm_content: "initial-utm-content-from-integrations-object", - initial_utm_id: "", - initial_utm_medium: "", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", initial_utm_source: "initial-utm-source-from-integrations-object", initial_utm_term: "initial-utm-term-from-integrations-object", - initial_wbraid: "", + initial_wbraid: "EMPTY", + }, + $unset: { + utm_medium: '-', + fbclid: '-', + dclid: '-', + gbraid: '-', + wbraid: '-', + ko_clickid: '-', + li_fat_id: '-', + msclkid: '-', + rtd_cid: '-', + ttclid: '-', + twclid: '-', + utm_id: '-', }, - $unset: [ - "utm_medium", - "fbclid", - "dclid", - "gbraid", - "wbraid", - "ko_clickid", - "li_fat_id", - "msclkid", - "rtd_cid", - "ttclid", - "twclid", - "utm_id", - ], "some-trait-key": "some-trait-value", }, }, @@ -211,9 +211,9 @@ describe('Amplitude', () => { 'Actions Amplitude': { autocapture_attribution: { enabled: true, - set_once: {}, // no attribution values provided - should still block mapped values + set_once: {}, // no attribution values provided, however we'll ignore the mappged values as enabled is true set: {}, - unset: [] + unset: {} } } }, @@ -288,7 +288,7 @@ describe('Amplitude', () => { }) }) - it('regular utm and referrer data is sent when autocapture attribution is disabled', async () => { + it('regular mapped utm and referrer data is sent when autocapture attribution is disabled', async () => { nock('https://api2.amplitude.com/2').post('/httpapi').reply(200, {}) const event = createTestEvent({ @@ -309,7 +309,7 @@ describe('Amplitude', () => { // enabled: true, // Disabled autocapture attribution set_once: {}, set: {}, - unset: [] + unset: {} } } }, diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts index c21e258c4ba..4daa10d68a6 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts @@ -40,9 +40,7 @@ export function getUserProperties(payload: Payload): { [k: string]: unknown } { ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } : setAlways } : {}), ...(compact(add) ? { $add: add } : {}), - ...(autocaptureAttributionUnset && autocaptureAttributionUnset.length > 0 - ? { $unset: autocaptureAttributionUnset } - : {}) + ...(compact(autocaptureAttributionUnset) ? { $unset: autocaptureAttributionUnset } : {}), } return userProperties diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts index 3807eb8d414..ff7ad5486ba 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts @@ -26,8 +26,7 @@ export const autocaptureFields: Record = { autocaptureAttributionUnset: { label: 'Autocapture Attribution Unset', description: 'Utility field used to detect if any attribution values need to be unset.', - type: 'string', - multiple: true, + type: 'object', default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, readOnly: true } diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 56f342c51d4..44d344d64b2 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -196,7 +196,9 @@ export interface Payload { /** * Utility field used to detect if any attribution values need to be unset. */ - autocaptureAttributionUnset?: string[] + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** * The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. */ From ff339fe1c8b4bd57c680232ac5dd08bd6edcc2b3 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 13:45:51 +0000 Subject: [PATCH 25/38] working unit tests --- .../__tests__/autocapture-attribution.test.ts | 577 ++++++++++++++++++ .../autocapture-attribution-functions.ts | 80 +++ .../src/sessionId/generated-types.ts | 8 + .../amplitude-plugins/src/sessionId/index.ts | 123 +--- .../src/sessionId/sessionid-functions.ts | 100 +++ 5 files changed, 796 insertions(+), 92 deletions(-) create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution-functions.ts create mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts new file mode 100644 index 00000000000..7073f804980 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts @@ -0,0 +1,577 @@ +import { Analytics, Context, Plugin } from '@segment/analytics-next' +import { Subscription } from '@segment/browser-destination-runtime/types' +import browserPluginsDestination from '../..' +import { DESTINATION_INTEGRATION_NAME } from '../../constants' + +describe('ajs-integration', () => { + describe('New session, with no attribution values in the URL', () => { + + const example: Subscription[] = [ + { + partnerAction: 'sessionId', + name: 'Session and Autocapture Attribution Plugin', + enabled: true, + subscribe: 'type = "track"', + mapping: { + enableAutocaptureAttribution: true + } + } + ] + + let browserActions: Plugin[] + let autocaptureAttributionPlugin: Plugin + let ajs: Analytics + + beforeAll(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + autocaptureAttributionPlugin = browserActions[0] + + ajs = new Analytics({ + writeKey: 'w_123' + }) + + Object.defineProperty(window, 'location', { + value: { + search: '?' + }, + writable: true + }) + }) + + afterAll(() => { + + }) + + test('updates the original event with with attributions values from the URL, caches the values, then updates when new values come along', async () => { + await autocaptureAttributionPlugin.load(Context.system(), ajs) + const ctx = new Context({ + type: 'track', + event: 'Test Event', + properties: { + greeting: 'Yo!' + } + }) + + /* + * Event will include empty attribution values since this is a new session and there are no attribution values in the URL + */ + const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj).toEqual({ + autocapture_attribution: { + enabled: true, + set: {}, + set_once: { + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer: "EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", + initial_utm_source: "EMPTY", + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" + }, + unset: { + dclid: "-", + fbclid: "-", + gbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + referrer: "-", + referring_domain: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-", + utm_campaign: "-", + utm_content: "-", + utm_id: "-", + utm_medium: "-", + utm_source: "-", + utm_term: "-", + wbraid: "-" + } + } + }) + }) + }) + + describe('autocapture works as expected', () => { + + const example: Subscription[] = [ + { + partnerAction: 'sessionId', + name: 'Session and Autocapture Attribution Plugin', + enabled: true, + subscribe: 'type = "track"', + mapping: { + enableAutocaptureAttribution: true + } + } + ] + + let browserActions: Plugin[] + let autocaptureAttributionPlugin: Plugin + let ajs: Analytics + + beforeAll(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + autocaptureAttributionPlugin = browserActions[0] + + ajs = new Analytics({ + writeKey: 'w_123' + }) + + // window.localStorage.clear() + + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale&utm_term=running+shoes&utm_content=ad1&gclid=gclid1234&gbraid=gbraid5678' + }, + writable: true + }) + }) + + test('updates the original event with with attributions values from the URL, caches the values, then updates when new values come along', async () => { + await autocaptureAttributionPlugin.load(Context.system(), ajs) + const ctx = new Context({ + type: 'track', + event: 'Test Event', + properties: { + greeting: 'Yo!' + } + }) + + /* + * First event on the page with attribution values will be transmitted with set and set_once values + */ + const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj).toEqual({ + autocapture_attribution: { + enabled: true, + set: { + gbraid: "gbraid5678", + gclid: "gclid1234", + utm_campaign: "spring_sale", + utm_content: "ad1", + utm_medium: "cpc", + utm_source: "google", + utm_term: "running shoes", + }, + set_once: { + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "gbraid5678", + initial_gclid: "gclid1234", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer: "EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", + initial_utm_campaign: "spring_sale", + initial_utm_content: "ad1", + initial_utm_id: "EMPTY", + initial_utm_medium: "cpc", + initial_utm_source: "google", + initial_utm_term: "running shoes", + initial_wbraid: "EMPTY" + }, + unset: { + referrer: "-", + referring_domain: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + wbraid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-" + } + } + }) + + /* + * Second event on the same page with attribution values will be transmitted without set and set_once values + */ + const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj1).toEqual({ + autocapture_attribution: { + enabled: true, + set_once: {}, + set: {}, + unset: {} + } + }) + + + /* + * A new URL should result in updated set and unset values being sent in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=email' + }, + writable: true + }) + + const updatedCtx2 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj2 = updatedCtx2?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj2).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + utm_source: "email", + }, + set_once: { + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer:"EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", + initial_utm_source: "email", + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" + }, + unset: { + referrer: "-", + referring_domain: "-", + utm_medium: "-", + utm_campaign: "-", + utm_term: "-", + utm_content: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + gbraid: "-", + wbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-" + } + } + } + ) + + /* + * Next a new page load happens which does not have any valid attribution values. No attribution values should be sent in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?' + }, + writable: true + }) + + const updatedCtx3 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj3 = updatedCtx3?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj3).toEqual( + { + autocapture_attribution: { + enabled: true, + set: {}, + set_once: {}, + unset: {} + } + } + ) + + + /* + * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload + */ + Object.defineProperty(window, 'location', { + value: { + search: '?some_fake_non_attribution_param=12345' + }, + writable: true + }) + + const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj4).toEqual( + { + autocapture_attribution: { + enabled: true, + set: {}, + set_once: {}, + unset: {} + } + } + ) + + /* + * Next we test with a completely new attribution parameter + */ + Object.defineProperty(window, 'location', { + value: { + search: '?ttclid=uyiuyiuy' + }, + writable: true + }) + + const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj5).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + ttclid: "uyiuyiuy" + }, + set_once: { + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer: "EMPTY", + initial_referring_domain: "EMPTY", + initial_rtd_cid: "EMPTY", + initial_ttclid: "uyiuyiuy", + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", + initial_utm_source: "EMPTY", + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" + }, + unset: { + referrer: "-", + referring_domain: "-", + utm_source: "-", + utm_medium: "-", + utm_campaign: "-", + utm_term: "-", + utm_content: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + gbraid: "-", + wbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + twclid: "-" + } + } + } + ) + + /* + * Next we test with a some attributes we've never seen before, referrer and referring_domain + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://blah.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) + + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://blah.com/path/page/hello/', + }) + + const updatedCtx6 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj6 = updatedCtx6?.event?.integrations[DESTINATION_INTEGRATION_NAME] + + expect(ampIntegrationsObj6).toEqual( + { + autocapture_attribution: { + enabled: true, + set: { + referrer: "https://blah.com/path/page/hello/", + referring_domain: "blah.com" + }, + set_once: { + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "EMPTY", + initial_ko_clickid: "EMPTY", + initial_li_fat_id: "EMPTY", + initial_msclkid: "EMPTY", + initial_referrer: "https://blah.com/path/page/hello/", + initial_referring_domain: "blah.com", + initial_rtd_cid: "EMPTY", + initial_ttclid: "EMPTY", + initial_twclid: "EMPTY", + initial_utm_campaign: "EMPTY", + initial_utm_content: "EMPTY", + initial_utm_id: "EMPTY", + initial_utm_medium: "EMPTY", + initial_utm_source: "EMPTY", + initial_utm_term: "EMPTY", + initial_wbraid: "EMPTY" + }, + unset: { + utm_source: "-", + utm_medium: "-", + utm_campaign: "-", + utm_term: "-", + utm_content: "-", + utm_id: "-", + dclid: "-", + fbclid: "-", + gbraid: "-", + wbraid: "-", + gclid: "-", + ko_clickid: "-", + li_fat_id: "-", + msclkid: "-", + rtd_cid: "-", + ttclid: "-", + twclid: "-" + } + } + } + ) + }) + }) + + describe('autocapture can be blocked as expected', () => { + + const example: Subscription[] = [ + { + partnerAction: 'autocaptureAttribution', + name: 'Autocapture Attribution Plugin', + enabled: true, + subscribe: 'type = "track"', + mapping: { + enableAutocaptureAttribution: true, + excludeReferrers: ["test.com", "sub.blah.com", "meh.blah.com"] + } + } + ] + + let browserActions: Plugin[] + let autocaptureAttributionPlugin: Plugin + let ajs: Analytics + + beforeAll(async () => { + browserActions = await browserPluginsDestination({ subscriptions: example }) + autocaptureAttributionPlugin = browserActions[0] + + ajs = new Analytics({ + writeKey: 'w_123' + }) + }) + + test('should prevent attribution from blocked domain', async () => { + await autocaptureAttributionPlugin.load(Context.system(), ajs) + const ctx = new Context({ + type: 'track', + event: 'Test Event', + properties: { + greeting: 'Yo!' + } + }) + + /* + * Page referrer is in the exclude list - no autocaptured attribution values should be sent. + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://test.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) + + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://test.com/path/page/hello/', + }) + + const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj).toEqual({ + autocapture_attribution: { + enabled: true, + set: {}, + set_once: {}, + unset: {} + } + }) + + /* + * Same test as before but this time with a sub domain + */ + Object.defineProperty(window, 'location', { + value: { + href: 'https://sub.blah.com/path/page/hello/', + search: '', + protocol: 'https:' + }, + writable: true + }) + + Object.defineProperty(document, 'referrer', { + writable: true, + value: 'https://sub.blah.com/path/page/hello/', + }) + + const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) + const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] + expect(ampIntegrationsObj1).toEqual({ + autocapture_attribution: { + enabled: true, + set: {}, + set_once: {}, + unset: {} + } + }) + + }) + }) +}) \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution-functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution-functions.ts new file mode 100644 index 00000000000..b52c09c9d58 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/autocapture-attribution-functions.ts @@ -0,0 +1,80 @@ +import type { Payload } from './generated-types' +import isEqual from 'lodash/isEqual' +import { DESTINATION_INTEGRATION_NAME } from '../constants' +import { + UniversalStorage, + Analytics, + Context +} from '@segment/analytics-next' +import { + AMPLITUDE_ATTRIBUTION_STORAGE_KEY, + AmplitudeAttributionValues, + AMPLITUDE_ATTRIBUTION_KEYS, + AmplitudeSetOnceAttributionValues, + AmplitudeAttributionUnsetValues +} from '@segment/actions-shared' + +export function enrichWithAutocaptureAttribution(context: Context, payload: Payload, analytics: Analytics, isNewSession: boolean): void { + console.log(isNewSession, isNewSession, isNewSession, isNewSession) + + const referrer = document.referrer + const referringDomain = referrer ? new URL(referrer).hostname : '' + const { excludeReferrers } = payload + const isExcluded = excludeReferrers?.includes(referringDomain) + const current: Partial = isExcluded ? {} : {...getAttributionsFromURL(window.location.search), referrer, referring_domain: referringDomain} + const previous = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) + const setOnce: Partial = {} + const set: Partial = {} + const unset: Partial = {} + const currentPageHasAttribution = current && Object.values(current).some(v => typeof v === 'string' && v.length > 0) + + if ((currentPageHasAttribution && !isEqual(current, previous)) || isNewSession) { + AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { + setOnce[`initial_${key}`] = current[key]?.trim() || "EMPTY" + if(current[key]){ + set[key] = current[key] + } + else{ + unset[key] = '-' + } + }) + if(Object.entries(current).length >0) { + setAttributionsInStorage(analytics.storage as UniversalStorage>>, current) + } + } + + if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { + enabled: true, + set_once: setOnce, + set: set, + unset: unset + }) + } + + return +} + +function getAttributionsFromURL(queryString: string): Partial { + if (!queryString){ + return {} + } + + const urlParams = new URLSearchParams(queryString) + + return Object.fromEntries( + AMPLITUDE_ATTRIBUTION_KEYS + .map(key => [key, urlParams.get(key)] as const) + .filter(([, value]) => value !== null) + ) as Partial +} + +function getAttributionsFromStorage(storage: UniversalStorage>>): Partial { + const values = storage.get(AMPLITUDE_ATTRIBUTION_STORAGE_KEY) + return values ?? {} +} + +function setAttributionsInStorage(storage: UniversalStorage>>, attributions: Partial): void { + storage.set(AMPLITUDE_ATTRIBUTION_STORAGE_KEY, attributions) +} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts index 3620e616234..fbaaccc5264 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/generated-types.ts @@ -17,4 +17,12 @@ export interface Payload { * The event name to use for the session end event. */ sessionEndEvent?: string + /** + * If enabled, attribution details will be captured from the URL and attached to every Amplitude browser based event. + */ + enableAutocaptureAttribution?: boolean + /** + * A list of hostnames to ignore when capturing attribution data. If the current page referrer matches any of these hostnames, no attribution data will be captured from the URL. + */ + excludeReferrers?: string[] } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts index 98dfda94423..803dc5c43c2 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts @@ -1,57 +1,13 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ -import { UniversalStorage, Analytics } from '@segment/analytics-next' import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { DESTINATION_INTEGRATION_NAME } from '../constants' - -function newSessionId(): number { - return now() -} - -function startSession(analytics: Analytics, eventName = 'session_started') { - analytics - .track(eventName, {}) - .then(() => true) - .catch(() => true) -} - -function endSession(analytics: Analytics, eventName = 'session_ended') { - analytics - .track(eventName, {}) - .then(() => true) - .catch(() => true) -} - -const THIRTY_MINUTES = 30 * 60000 - -function withinSessionLimit(newTimeStamp: number, updated: number | null, length: number = THIRTY_MINUTES): boolean { - // This checks if the new timestamp is within the specified length of the last updated timestamp - const deltaTime = newTimeStamp - (updated ?? 0) - return deltaTime < length -} - -function now(): number { - return new Date().getTime() -} - -function stale(id: number | null, updated: number | null, length: number = THIRTY_MINUTES): id is null { - if (id === null || updated === null) { - return true - } - - const accessedAt = updated - - if (now() - accessedAt >= length) { - return true - } - - return false -} +import { enrichWithSessionId, THIRTY_MINUTES } from './sessionid-functions' +import { enrichWithAutocaptureAttribution } from './autocapture-attribution-functions' const action: BrowserActionDefinition = { - title: 'Session Plugin', - description: 'Generates a Session ID and attaches it to every Amplitude browser based event.', + title: 'Session and Autocapture Attribution Plugin', + description: 'Enriches events with session IDs and autocapture attribution data for Amplitude.', platform: 'web', defaultSubscription: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', fields: { @@ -101,55 +57,38 @@ const action: BrowserActionDefinition = { } ] } + }, + enableAutocaptureAttribution: { + label: 'Enable Autocapture Attribution', + type: 'boolean', + default: false, + required: false, + description: 'If enabled, attribution details will be captured from the URL and attached to every Amplitude browser based event.' + }, + excludeReferrers: { + label: 'Exclude Referrers', + description: 'A list of hostnames to ignore when capturing attribution data. If the current page referrer matches any of these hostnames, no attribution data will be captured from the URL.', + type: 'string', + required: false, + multiple: true, + depends_on: { + conditions: [ + { + fieldKey: 'enableAutocaptureAttribution', + operator: 'is', + value: true + } + ] + } } }, lifecycleHook: 'enrichment', perform: (_, { context, payload, analytics }) => { - // TODO: this can be removed when storage layer in AJS is rolled out to all customers - const storageFallback = { - get: (key: string) => { - const data = window.localStorage.getItem(key) - return data === null ? null : parseInt(data, 10) - }, - set: (key: string, value: number) => { - return window.localStorage.setItem(key, value.toString()) - } - } - - const newSession = newSessionId() - const storage = analytics.storage - ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - (analytics.storage as UniversalStorage>) - : storageFallback - - const raw = storage.get('analytics_session_id') - const updated = storage.get('analytics_session_id.last_access') - - const withInSessionLimit = withinSessionLimit(newSession, updated, payload.sessionLength) - if (!withInSessionLimit && payload.triggerSessionEvents) { - // end previous session - endSession(analytics, payload.sessionEndEvent) + const { enableAutocaptureAttribution } = payload + const isNewSession = enrichWithSessionId(context, payload, analytics) + if(enableAutocaptureAttribution){ + enrichWithAutocaptureAttribution(context, payload, analytics, isNewSession) } - - let id: number | null = raw - if (stale(raw, updated, payload.sessionLength)) { - id = newSession - storage.set('analytics_session_id', id) - if (payload.triggerSessionEvents) startSession(analytics, payload.sessionStartEvent) - } else { - // we are storing the session id regardless, so it gets synced between different storage mediums - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- id can't be null because of stale check - storage.set('analytics_session_id', id!) - } - - storage.set('analytics_session_id.last_access', newSession) - - if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { - context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) - context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.session_id`, id) - } - - return } } diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts new file mode 100644 index 00000000000..bcb986f71a5 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { UniversalStorage } from '@segment/analytics-next' +import type { Payload } from './generated-types' +import { Analytics, Context } from '@segment/analytics-next' +import { DESTINATION_INTEGRATION_NAME } from '../constants' + +export function enrichWithSessionId(context: Context, payload: Payload, analytics: Analytics): boolean { + // TODO: this can be removed when storage layer in AJS is rolled out to all customers + const storageFallback = { + get: (key: string) => { + const data = window.localStorage.getItem(key) + return data === null ? null : parseInt(data, 10) + }, + set: (key: string, value: number) => { + return window.localStorage.setItem(key, value.toString()) + } + } + + let isNewSession = false + + const newSession = newSessionId() + const storage = analytics.storage + ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (analytics.storage as UniversalStorage>) + : storageFallback + + const raw = storage.get('analytics_session_id') + const updated = storage.get('analytics_session_id.last_access') + + const withInSessionLimit = withinSessionLimit(newSession, updated, payload.sessionLength) + if (!withInSessionLimit && payload.triggerSessionEvents) { + // end previous session + endSession(analytics, payload.sessionEndEvent) + } + + let id: number | null = raw + if (stale(raw, updated, payload.sessionLength)) { + isNewSession = true + id = newSession + storage.set('analytics_session_id', id) + if (payload.triggerSessionEvents) startSession(analytics, payload.sessionStartEvent) + } else { + // we are storing the session id regardless, so it gets synced between different storage mediums + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- id can't be null because of stale check + storage.set('analytics_session_id', id!) + } + + storage.set('analytics_session_id.last_access', newSession) + + if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) + context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.session_id`, id) + } + console.log('isNewSession', isNewSession) + return isNewSession +} + +function newSessionId(): number { + return now() +} + +function startSession(analytics: Analytics, eventName = 'session_started') { + analytics + .track(eventName, {}) + .then(() => true) + .catch(() => true) +} + +function endSession(analytics: Analytics, eventName = 'session_ended') { + analytics + .track(eventName, {}) + .then(() => true) + .catch(() => true) +} + +export const THIRTY_MINUTES = 30 * 60000 + +function withinSessionLimit(newTimeStamp: number, updated: number | null, length: number = THIRTY_MINUTES): boolean { + // This checks if the new timestamp is within the specified length of the last updated timestamp + const deltaTime = newTimeStamp - (updated ?? 0) + return deltaTime < length +} + +function now(): number { + return new Date().getTime() +} + +function stale(id: number | null, updated: number | null, length: number = THIRTY_MINUTES): id is null { + if (id === null || updated === null) { + return true + } + + const accessedAt = updated + + if (now() - accessedAt >= length) { + return true + } + + return false +} \ No newline at end of file From 7b53befd747c8cda29549527464a43bde022b0a4 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 14:00:28 +0000 Subject: [PATCH 26/38] tests passing --- .../__tests__/index.test.ts | 469 ------------------ .../src/autocaptureAttribution/functions.ts | 27 - .../autocaptureAttribution/generated-types.ts | 8 - .../src/autocaptureAttribution/index.ts | 66 --- .../amplitude-plugins/src/index.ts | 4 +- .../__tests__/autocapture-attribution.test.ts | 2 +- .../amplitude-plugins/src/sessionId/index.ts | 4 +- .../src/sessionId/sessionid-functions.ts | 1 - 8 files changed, 4 insertions(+), 577 deletions(-) delete mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts delete mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts delete mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts delete mode 100644 packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts deleted file mode 100644 index e15618825de..00000000000 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/__tests__/index.test.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { Analytics, Context, Plugin } from '@segment/analytics-next' -import { Subscription } from '@segment/browser-destination-runtime/types' -import browserPluginsDestination from '../../' -import { DESTINATION_INTEGRATION_NAME } from '../../constants' - -describe('ajs-integration', () => { - describe('autocapture works as expected', () => { - - const example: Subscription[] = [ - { - partnerAction: 'autocaptureAttribution', - name: 'Autocapture Attribution Plugin', - enabled: true, - subscribe: 'type = "track"', - mapping: {} - } - ] - - let browserActions: Plugin[] - let autocaptureAttributionPlugin: Plugin - let ajs: Analytics - - beforeAll(async () => { - browserActions = await browserPluginsDestination({ subscriptions: example }) - autocaptureAttributionPlugin = browserActions[0] - - ajs = new Analytics({ - writeKey: 'w_123' - }) - - // window.localStorage.clear() - - Object.defineProperty(window, 'location', { - value: { - search: '?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale&utm_term=running+shoes&utm_content=ad1&gclid=gclid1234&gbraid=gbraid5678' - }, - writable: true - }) - }) - - test('updates the original event with with attributions values from the URL, caches the values, then updates when new values come along', async () => { - await autocaptureAttributionPlugin.load(Context.system(), ajs) - const ctx = new Context({ - type: 'track', - event: 'Test Event', - properties: { - greeting: 'Yo!' - } - }) - - /* - * First event on the page with attribution values will be transmitted with set and set_once values - */ - const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj).toEqual({ - autocapture_attribution: { - enabled: true, - set: { - gbraid: "gbraid5678", - gclid: "gclid1234", - utm_campaign: "spring_sale", - utm_content: "ad1", - utm_medium: "cpc", - utm_source: "google", - utm_term: "running shoes", - }, - set_once: { - initial_dclid: "EMPTY", - initial_fbclid: "EMPTY", - initial_gbraid: "gbraid5678", - initial_gclid: "gclid1234", - initial_ko_clickid: "EMPTY", - initial_li_fat_id: "EMPTY", - initial_msclkid: "EMPTY", - initial_referrer: "EMPTY", - initial_referring_domain: "EMPTY", - initial_rtd_cid: "EMPTY", - initial_ttclid: "EMPTY", - initial_twclid: "EMPTY", - initial_utm_campaign: "spring_sale", - initial_utm_content: "ad1", - initial_utm_id: "EMPTY", - initial_utm_medium: "cpc", - initial_utm_source: "google", - initial_utm_term: "running shoes", - initial_wbraid: "EMPTY" - }, - unset: { - referrer: "-", - referring_domain: "-", - utm_id: "-", - dclid: "-", - fbclid: "-", - wbraid: "-", - ko_clickid: "-", - li_fat_id: "-", - msclkid: "-", - rtd_cid: "-", - ttclid: "-", - twclid: "-" - } - } - }) - - /* - * Second event on the same page with attribution values will be transmitted without set and set_once values - */ - const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj1).toEqual({ - autocapture_attribution: { - enabled: true, - set_once: {}, - set: {}, - unset: {} - } - }) - - - /* - * A new URL should result in updated set and unset values being sent in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?utm_source=email' - }, - writable: true - }) - - const updatedCtx2 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj2 = updatedCtx2?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj2).toEqual( - { - autocapture_attribution: { - enabled: true, - set: { - utm_source: "email", - }, - set_once: { - initial_dclid: "EMPTY", - initial_fbclid: "EMPTY", - initial_gbraid: "EMPTY", - initial_gclid: "EMPTY", - initial_ko_clickid: "EMPTY", - initial_li_fat_id: "EMPTY", - initial_msclkid: "EMPTY", - initial_referrer:"EMPTY", - initial_referring_domain: "EMPTY", - initial_rtd_cid: "EMPTY", - initial_ttclid: "EMPTY", - initial_twclid: "EMPTY", - initial_utm_campaign: "EMPTY", - initial_utm_content: "EMPTY", - initial_utm_id: "EMPTY", - initial_utm_medium: "EMPTY", - initial_utm_source: "email", - initial_utm_term: "EMPTY", - initial_wbraid: "EMPTY" - }, - unset: { - referrer: "-", - referring_domain: "-", - utm_medium: "-", - utm_campaign: "-", - utm_term: "-", - utm_content: "-", - utm_id: "-", - dclid: "-", - fbclid: "-", - gbraid: "-", - wbraid: "-", - gclid: "-", - ko_clickid: "-", - li_fat_id: "-", - msclkid: "-", - rtd_cid: "-", - ttclid: "-", - twclid: "-" - } - } - } - ) - - /* - * Next a new page load happens which does not have any valid attribution values. No attribution values should be sent in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?' - }, - writable: true - }) - - const updatedCtx3 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj3 = updatedCtx3?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj3).toEqual( - { - autocapture_attribution: { - enabled: true, - set: {}, - set_once: {}, - unset: {} - } - } - ) - - - /* - * Then we test when there are non attreibution URL params - the last cached attribution values are passed correctly in the payload - */ - Object.defineProperty(window, 'location', { - value: { - search: '?some_fake_non_attribution_param=12345' - }, - writable: true - }) - - const updatedCtx4 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj4 = updatedCtx4?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj4).toEqual( - { - autocapture_attribution: { - enabled: true, - set: {}, - set_once: {}, - unset: {} - } - } - ) - - /* - * Next we test with a completely new attribution parameter - */ - Object.defineProperty(window, 'location', { - value: { - search: '?ttclid=uyiuyiuy' - }, - writable: true - }) - - const updatedCtx5 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj5 = updatedCtx5?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj5).toEqual( - { - autocapture_attribution: { - enabled: true, - set: { - ttclid: "uyiuyiuy" - }, - set_once: { - initial_dclid: "EMPTY", - initial_fbclid: "EMPTY", - initial_gbraid: "EMPTY", - initial_gclid: "EMPTY", - initial_ko_clickid: "EMPTY", - initial_li_fat_id: "EMPTY", - initial_msclkid: "EMPTY", - initial_referrer: "EMPTY", - initial_referring_domain: "EMPTY", - initial_rtd_cid: "EMPTY", - initial_ttclid: "uyiuyiuy", - initial_twclid: "EMPTY", - initial_utm_campaign: "EMPTY", - initial_utm_content: "EMPTY", - initial_utm_id: "EMPTY", - initial_utm_medium: "EMPTY", - initial_utm_source: "EMPTY", - initial_utm_term: "EMPTY", - initial_wbraid: "EMPTY" - }, - unset: { - referrer: "-", - referring_domain: "-", - utm_source: "-", - utm_medium: "-", - utm_campaign: "-", - utm_term: "-", - utm_content: "-", - utm_id: "-", - dclid: "-", - fbclid: "-", - gbraid: "-", - wbraid: "-", - gclid: "-", - ko_clickid: "-", - li_fat_id: "-", - msclkid: "-", - rtd_cid: "-", - twclid: "-" - } - } - } - ) - - /* - * Next we test with a some attributes we've never seen before, referrer and referring_domain - */ - Object.defineProperty(window, 'location', { - value: { - href: 'https://blah.com/path/page/hello/', - search: '', - protocol: 'https:' - }, - writable: true - }) - - Object.defineProperty(document, 'referrer', { - writable: true, - value: 'https://blah.com/path/page/hello/', - }) - - const updatedCtx6 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj6 = updatedCtx6?.event?.integrations[DESTINATION_INTEGRATION_NAME] - - expect(ampIntegrationsObj6).toEqual( - { - autocapture_attribution: { - enabled: true, - set: { - referrer: "https://blah.com/path/page/hello/", - referring_domain: "blah.com" - }, - set_once: { - initial_dclid: "EMPTY", - initial_fbclid: "EMPTY", - initial_gbraid: "EMPTY", - initial_gclid: "EMPTY", - initial_ko_clickid: "EMPTY", - initial_li_fat_id: "EMPTY", - initial_msclkid: "EMPTY", - initial_referrer: "https://blah.com/path/page/hello/", - initial_referring_domain: "blah.com", - initial_rtd_cid: "EMPTY", - initial_ttclid: "EMPTY", - initial_twclid: "EMPTY", - initial_utm_campaign: "EMPTY", - initial_utm_content: "EMPTY", - initial_utm_id: "EMPTY", - initial_utm_medium: "EMPTY", - initial_utm_source: "EMPTY", - initial_utm_term: "EMPTY", - initial_wbraid: "EMPTY" - }, - unset: { - utm_source: "-", - utm_medium: "-", - utm_campaign: "-", - utm_term: "-", - utm_content: "-", - utm_id: "-", - dclid: "-", - fbclid: "-", - gbraid: "-", - wbraid: "-", - gclid: "-", - ko_clickid: "-", - li_fat_id: "-", - msclkid: "-", - rtd_cid: "-", - ttclid: "-", - twclid: "-" - } - } - } - ) - }) - }) - describe('autocapture can be blocked as expected', () => { - - const example: Subscription[] = [ - { - partnerAction: 'autocaptureAttribution', - name: 'Autocapture Attribution Plugin', - enabled: true, - subscribe: 'type = "track"', - mapping: { - excludeReferrers: ["test.com", "sub.blah.com", "meh.blah.com"] - } - } - ] - - let browserActions: Plugin[] - let autocaptureAttributionPlugin: Plugin - let ajs: Analytics - - beforeAll(async () => { - browserActions = await browserPluginsDestination({ subscriptions: example }) - autocaptureAttributionPlugin = browserActions[0] - - ajs = new Analytics({ - writeKey: 'w_123' - }) - }) - - test('should prevent attribution from blocked domain', async () => { - await autocaptureAttributionPlugin.load(Context.system(), ajs) - const ctx = new Context({ - type: 'track', - event: 'Test Event', - properties: { - greeting: 'Yo!' - } - }) - - /* - * Page referrer is in the exclude list - no autocaptured attribution values should be sent. - */ - Object.defineProperty(window, 'location', { - value: { - href: 'https://test.com/path/page/hello/', - search: '', - protocol: 'https:' - }, - writable: true - }) - - Object.defineProperty(document, 'referrer', { - writable: true, - value: 'https://test.com/path/page/hello/', - }) - - const updatedCtx = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj = updatedCtx?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj).toEqual({ - autocapture_attribution: { - enabled: true, - set: {}, - set_once: {}, - unset: {} - } - }) - - /* - * Same test as before but this time with a sub domain - */ - Object.defineProperty(window, 'location', { - value: { - href: 'https://sub.blah.com/path/page/hello/', - search: '', - protocol: 'https:' - }, - writable: true - }) - - Object.defineProperty(document, 'referrer', { - writable: true, - value: 'https://sub.blah.com/path/page/hello/', - }) - - const updatedCtx1 = await autocaptureAttributionPlugin.track?.(ctx) - const ampIntegrationsObj1 = updatedCtx1?.event?.integrations[DESTINATION_INTEGRATION_NAME] - expect(ampIntegrationsObj1).toEqual({ - autocapture_attribution: { - enabled: true, - set: {}, - set_once: {}, - unset: {} - } - }) - - }) - }) -}) - diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts deleted file mode 100644 index 38b1bd16db0..00000000000 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/functions.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import { UniversalStorage } from '@segment/analytics-next' -import type { AmplitudeAttributionValues } from '@segment/actions-shared/src/amplitude/types' -import { AMPLITUDE_ATTRIBUTION_KEYS, AMPLITUDE_ATTRIBUTION_STORAGE_KEY } from '@segment/actions-shared' - -export function getAttributionsFromURL(queryString: string): Partial { - if (!queryString){ - return {} - } - - const urlParams = new URLSearchParams(queryString) - - return Object.fromEntries( - AMPLITUDE_ATTRIBUTION_KEYS - .map(key => [key, urlParams.get(key)] as const) - .filter(([, value]) => value !== null) - ) as Partial -} - -export function getAttributionsFromStorage(storage: UniversalStorage>>): Partial { - const values = storage.get(AMPLITUDE_ATTRIBUTION_STORAGE_KEY) - return values ?? {} -} - -export function setAttributionsInStorage(storage: UniversalStorage>>, attributions: Partial): void { - storage.set(AMPLITUDE_ATTRIBUTION_STORAGE_KEY, attributions) -} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts deleted file mode 100644 index 29e6cfb34b1..00000000000 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/generated-types.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Generated file. DO NOT MODIFY IT BY HAND. - -export interface Payload { - /** - * A list of hostnames to ignore when capturing attribution data. If the current page referrer matches any of these hostnames, no attribution data will be captured from the URL. - */ - excludeReferrers?: string[] -} diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts deleted file mode 100644 index 1591efcdbb9..00000000000 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/autocaptureAttribution/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -import { UniversalStorage } from '@segment/analytics-next' -import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' -import type { Settings } from '../generated-types' -import type { Payload } from './generated-types' -import { getAttributionsFromURL, getAttributionsFromStorage, setAttributionsInStorage } from './functions' -import { AmplitudeAttributionValues, AMPLITUDE_ATTRIBUTION_KEYS, AmplitudeSetOnceAttributionValues, AmplitudeAttributionUnsetValues } from '@segment/actions-shared' -import { DESTINATION_INTEGRATION_NAME } from '../constants' -import isEqual from 'lodash/isEqual' - -const action: BrowserActionDefinition = { - title: 'Autocapture Attribution Plugin', - description: 'Captures attribution details from the URL and attaches them to every Amplitude browser based event. Use with the Log Event V2 action to automate the collection of attribution data.', - platform: 'web', - defaultSubscription: 'type = "track"', - fields: { - excludeReferrers: { - label: 'Exclude Referrers', - description: 'A list of hostnames to ignore when capturing attribution data. If the current page referrer matches any of these hostnames, no attribution data will be captured from the URL.', - type: 'string', - required: false, - multiple: true - } - }, - lifecycleHook: 'enrichment', - perform: (_, { context, payload, analytics }) => { - const referrer = document.referrer - const referringDomain = referrer ? new URL(referrer).hostname : '' - const { excludeReferrers } = payload - const isExcluded = excludeReferrers?.includes(referringDomain) - const current: Partial = isExcluded ? {} : {...getAttributionsFromURL(window.location.search), referrer, referring_domain: referringDomain} - const previous = getAttributionsFromStorage(analytics.storage as UniversalStorage>>) - const setOnce: Partial = {} - const set: Partial = {} - const unset: Partial = {} - const currentPageHasAttribution = current && Object.values(current).some(v => typeof v === 'string' && v.length > 0) - - if (currentPageHasAttribution && !isEqual(current, previous)){ - AMPLITUDE_ATTRIBUTION_KEYS.forEach(key => { - setOnce[`initial_${key}`] = current[key]?.trim() || "EMPTY" - if(current[key]){ - set[key] = current[key] - } - else{ - unset[key] = '-' - } - }) - if(Object.entries(current).length >0) { - setAttributionsInStorage(analytics.storage as UniversalStorage>>, current) - } - } - - if (context.event.integrations?.All !== false || context.event.integrations[DESTINATION_INTEGRATION_NAME]) { - context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) - context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution`, { - enabled: true, - set_once: setOnce, - set: set, - unset: unset - }) - } - - return - } -} -export default action \ No newline at end of file diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts index 9a19c414f9d..9227d87747b 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/index.ts @@ -2,14 +2,12 @@ import type { Settings } from './generated-types' import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types' import { browserDestination } from '@segment/browser-destination-runtime/shim' import sessionId from './sessionId' -import autocaptureAttribution from './autocaptureAttribution' export const destination: BrowserDestinationDefinition = { name: 'Amplitude (Actions)', mode: 'device', actions: { - sessionId, - autocaptureAttribution + sessionId }, initialize: async () => { return {} diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts index 7073f804980..ecc82adeabd 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/__tests__/autocapture-attribution.test.ts @@ -482,7 +482,7 @@ describe('ajs-integration', () => { const example: Subscription[] = [ { - partnerAction: 'autocaptureAttribution', + partnerAction: 'sessionId', name: 'Autocapture Attribution Plugin', enabled: true, subscribe: 'type = "track"', diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts index 803dc5c43c2..dece724f2aa 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/index.ts @@ -60,10 +60,10 @@ const action: BrowserActionDefinition = { }, enableAutocaptureAttribution: { label: 'Enable Autocapture Attribution', + description: 'If enabled, attribution details will be captured from the URL and attached to every Amplitude browser based event.', type: 'boolean', default: false, - required: false, - description: 'If enabled, attribution details will be captured from the URL and attached to every Amplitude browser based event.' + required: false }, excludeReferrers: { label: 'Exclude Referrers', diff --git a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts index bcb986f71a5..565d2357a29 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts @@ -51,7 +51,6 @@ export function enrichWithSessionId(context: Context, payload: Payload, analytic context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}`, {}) context.updateEvent(`integrations.${DESTINATION_INTEGRATION_NAME}.session_id`, id) } - console.log('isNewSession', isNewSession) return isNewSession } From 3762e4c923457cd0aa9aa4c34428dc34c603cfd9 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 15:32:53 +0000 Subject: [PATCH 27/38] refactor --- .../amplitude/__tests__/amplitude.test.ts | 8 -- .../logEventV2/autocapture-attribution.ts | 17 ++--- .../logEventV2/autocapture-fields.ts | 2 +- .../amplitude/logEventV2/constants.ts | 4 + .../amplitude/logEventV2/index.ts | 75 +++++++------------ .../amplitude/logEventV2/types.ts | 63 ++++++++++++++++ .../src/destinations/amplitude/user-agent.ts | 2 +- 7 files changed, 102 insertions(+), 69 deletions(-) create mode 100644 packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts index 374c9490bd0..781539fdf70 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts @@ -1591,9 +1591,7 @@ describe('Amplitude', () => { "events": Array [ Object { "device_id": "6fd32a7e-3c56-44c2-bd32-62bbec44c53d", - "device_manufacturer": undefined, "device_model": "Mac OS", - "device_type": undefined, "event_properties": Object {}, "event_type": "Test Event", "library": "segment", @@ -1605,7 +1603,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1659,7 +1656,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1713,7 +1709,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1905,9 +1900,7 @@ describe('Amplitude', () => { "events": Array [ Object { "device_id": "6fd32a7e-3c56-44c2-bd32-62bbec44c53d", - "device_manufacturer": undefined, "device_model": "Mac OS", - "device_type": undefined, "event_properties": Object {}, "event_type": "Test Event", "library": "segment", @@ -1919,7 +1912,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts index 4daa10d68a6..f968286062e 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts @@ -1,13 +1,12 @@ import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' import { Payload } from './generated-types' - -export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' +import { UserProperties } from './types' function compact(object: { [k: string]: unknown } | undefined): boolean { return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 } -export function getUserProperties(payload: Payload): { [k: string]: unknown } { +export function getUserProperties(payload: Payload): UserProperties { const { setOnce, setAlways, @@ -33,14 +32,14 @@ export function getUserProperties(payload: Payload): { [k: string]: unknown } { const userProperties = { ...user_properties, - ...(compact(autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } : setOnce) - ? { $setOnce: autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } : setOnce } + ...(compact(autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } as { [k: string]: string } : setOnce as { [k: string]: string }) + ? { $setOnce: autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } as { [k: string]: string }: setOnce as { [k: string]: string }} : {}), - ...(compact(autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } : setAlways) - ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } : setAlways } + ...(compact(autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }) + ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }} : {}), - ...(compact(add) ? { $add: add } : {}), - ...(compact(autocaptureAttributionUnset) ? { $unset: autocaptureAttributionUnset } : {}), + ...(compact(add) ? { $add: add as { [k: string]: string } } : {}), + ...(compact(autocaptureAttributionUnset) ? { $unset: autocaptureAttributionUnset as { [k: string]: string } } : {}), } return userProperties diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts index ff7ad5486ba..9e69d757a28 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts @@ -1,5 +1,5 @@ import type { InputField } from '@segment/actions-core' -import { DESTINATION_INTEGRATION_NAME } from './autocapture-attribution' +import { DESTINATION_INTEGRATION_NAME } from './constants' export const autocaptureFields: Record = { autocaptureAttributionEnabled: { diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts new file mode 100644 index 00000000000..e09b8ad3529 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts @@ -0,0 +1,4 @@ +const REVENUE_KETS = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] +const USER_PROPERTY_KEYS = ['setOnce', 'setAlways','add','autocaptureAttributionEnabled','autocaptureAttributionSet','autocaptureAttributionSetOnce','autocaptureAttributionUnset'] +export const KEYS_TO_OMIT = [...REVENUE_KETS, ...USER_PROPERTY_KEYS] +export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index e05273db7cf..8abe18a6831 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -9,19 +9,9 @@ import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' import { autocaptureFields } from './autocapture-fields' import { getUserProperties } from './autocapture-attribution' +import { AmplitudeEvent, JSON } from './types' +import { KEYS_TO_OMIT } from './constants' -export interface AmplitudeEvent extends Omit { - library?: string - time?: number - session_id?: number - options?: { - min_id_length: number - } -} - -const revenueKeys = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] -const userPropertyKeys = ['setOnce', 'setAlways','add','autocaptureAttributionEnabled','autocaptureAttributionSet','autocaptureAttributionSetOnce','autocaptureAttributionUnset'] -const keysToOmit = [...revenueKeys, ...userPropertyKeys] const action: ActionDefinition = { title: 'Log Event V2', description: 'Send an event to Amplitude', @@ -224,55 +214,40 @@ const action: ActionDefinition = { userAgentParsing, includeRawUserAgent, userAgentData, - min_id_length, + min_id_length, + platform, library, + user_id, ...rest - } = omit(payload, keysToOmit) - const properties = rest as AmplitudeEvent - let options + } = omit(payload, KEYS_TO_OMIT) - if (properties.platform) { - properties.platform = properties.platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') - } + const user_properties = getUserProperties(payload) - if (library === 'analytics.js' && !properties.platform) { - properties.platform = 'Web' + const event: AmplitudeEvent = { + ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), + ...(includeRawUserAgent && { user_agent: userAgent }), + ...rest, + ...{ user_id: user_id || null }, + ...(platform ? { platform: platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } : {}), + ...(library === 'analytics.js' && !platform ? { platform: 'Web' } : {}), + ...(time && dayjs.utc(time).isValid() ? { time: dayjs.utc(time).valueOf() } : {}), + ...(session_id && dayjs.utc(session_id).isValid() ? { session_id: formatSessionId(session_id) } : {}), + ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}), + ...(user_properties ? { user_properties } : {}), + library: 'segment' } - if (time && dayjs.utc(time).isValid()) { - properties.time = dayjs.utc(time).valueOf() + const json: JSON = { + api_key: settings.apiKey, + events: [removeUndefined(event)], + ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}) } - - if (session_id && dayjs.utc(session_id).isValid()) { - properties.session_id = formatSessionId(session_id) - } - - if (min_id_length && min_id_length > 0) { - options = { min_id_length } - } - - properties.user_properties = getUserProperties(payload) - - const events: AmplitudeEvent[] = [ - { - // Conditionally parse user agent using amplitude's library - ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), - ...(includeRawUserAgent && { user_agent: userAgent }), - // Make sure any top-level properties take precedence over user-agent properties - ...removeUndefined(properties), - library: 'segment' - } - ] - + const endpoint = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) return request(endpoint, { method: 'post', - json: { - api_key: settings.apiKey, - events, - options - } + json }) } } diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts new file mode 100644 index 00000000000..4ee6465e1ee --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts @@ -0,0 +1,63 @@ +import { ParsedUA } from '../user-agent' + +export interface AmplitudeEvent { + user_id?: string | null + device_id?: string + event_type: string + session_id?: number + time?: number + event_properties?: Record + user_properties?: UserProperties + groups?: Record + app_version?: string + platform?: string + os_name?: string + os_version?: string + device_brand?: string + device_manufacturer?: string + device_model?: string + carrier?: string + country?: string + region?: string + city?: string + dma?: string + language?: string + price?: number + quantity?: number + revenue?: number + productId?: string + revenueType?: string + location_lat?: number + location_lng?: number + ip?: string + idfa?: string + idfv?: string + adid?: string + android_id?: string + event_id?: number + insert_id?: string + library?: string + use_batch_endpoint?: boolean + user_agent?: ParsedUA | string + includeRawUserAgent?: boolean + userAgentData?: { + model?: string + platformVersion?: string + } +} + +export interface UserProperties { + $set?: { [k: string]: string } + $setOnce?: { [k: string]: string } + $unset?: { [k: string]: string } + $add?: { [k: string]: string } + [k: string]: unknown +} + +export interface JSON { + api_key: string + events: AmplitudeEvent[] + options?: { + min_id_length: number + } +} diff --git a/packages/destination-actions/src/destinations/amplitude/user-agent.ts b/packages/destination-actions/src/destinations/amplitude/user-agent.ts index b4c13a4a3a6..95f82847469 100644 --- a/packages/destination-actions/src/destinations/amplitude/user-agent.ts +++ b/packages/destination-actions/src/destinations/amplitude/user-agent.ts @@ -1,6 +1,6 @@ import UaParser from '@amplitude/ua-parser-js' -interface ParsedUA { +export interface ParsedUA { os_name?: string os_version?: string device_model?: string From a3f5dc96bc7aa155b7eda2f17a4a13e3533979a8 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 28 Nov 2025 10:53:36 +0000 Subject: [PATCH 28/38] logpurchase action converted --- .../amplitude/__tests__/amplitude.test.ts | 8 - .../logEventV2/autocapture-attribution.ts | 139 +++++++++++-- .../amplitude/logEventV2/constants.ts | 4 +- .../amplitude/logEventV2/index.ts | 53 +---- .../amplitude/logEventV2/types.ts | 23 ++- .../amplitude/logPurchase/generated-types.ts | 38 +++- .../amplitude/logPurchase/index.ts | 191 ++++-------------- 7 files changed, 206 insertions(+), 250 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts index 781539fdf70..0d45146c848 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts @@ -380,9 +380,7 @@ describe('Amplitude', () => { "events": Array [ Object { "device_id": "6fd32a7e-3c56-44c2-bd32-62bbec44c53d", - "device_manufacturer": undefined, "device_model": "Mac OS", - "device_type": undefined, "event_properties": Object {}, "event_type": "Test Event", "library": "segment", @@ -394,7 +392,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -448,7 +445,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -502,7 +498,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -597,9 +592,7 @@ describe('Amplitude', () => { "events": Array [ Object { "device_id": "6fd32a7e-3c56-44c2-bd32-62bbec44c53d", - "device_manufacturer": undefined, "device_model": "Mac OS", - "device_type": undefined, "event_properties": Object {}, "event_type": "Test Event", "library": "segment", @@ -611,7 +604,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts index f968286062e..bb91a19030e 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts @@ -1,16 +1,82 @@ import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' -import { Payload } from './generated-types' -import { UserProperties } from './types' +import { Payload as LogEventV2Payload} from './generated-types' +import { Payload as PurchasePayload } from '../logPurchase/generated-types' +import { UserProperties, EventRevenue, AmplitudeEvent, JSON } from './types' +import { RequestClient, omit, removeUndefined } from '@segment/actions-core' +import { Settings } from '../generated-types' +import { KEYS_TO_OMIT } from './constants' +import { parseUserAgentProperties } from '../user-agent' +import { formatSessionId } from '../convert-timestamp' +import { getEndpointByRegion } from '../regional-endpoints' +import dayjs from '../../../lib/dayjs' -function compact(object: { [k: string]: unknown } | undefined): boolean { - return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 +export function send(request: RequestClient, payload: LogEventV2Payload | PurchasePayload, settings: Settings, isPurchaseEvent: boolean) { + const { + time, + session_id, + userAgent, + userAgentParsing, + includeRawUserAgent, + userAgentData, + min_id_length, + platform, + library, + user_id, + products = [], + ...rest + } = omit(payload, KEYS_TO_OMIT) + + let trackRevenuePerProduct = false + if ('trackRevenuePerProduct' in payload) { + trackRevenuePerProduct = payload.trackRevenuePerProduct || false + } + + const user_properties = getUserProperties(payload) + + const events: AmplitudeEvent[] = [{ + ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), + ...(includeRawUserAgent && { user_agent: userAgent }), + ...rest, + ...{ user_id: user_id || null }, + ...(platform ? { platform: platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } : {}), + ...(library === 'analytics.js' && !platform ? { platform: 'Web' } : {}), + ...(time && dayjs.utc(time).isValid() ? { time: dayjs.utc(time).valueOf() } : {}), + ...(session_id && dayjs.utc(session_id).isValid() ? { session_id: formatSessionId(session_id) } : {}), + ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}), + ...(user_properties ? { user_properties } : {}), + ...(products.length && trackRevenuePerProduct ? {} : getRevenueProperties(payload)), + library: 'segment' + }] + + if(isPurchaseEvent){ + const mainEvent = events[0] + for (const product of products) { + events.push({ + ...mainEvent, + ...(trackRevenuePerProduct ? getRevenueProperties(product as EventRevenue) : {}), + event_properties: product, + event_type: 'Product Purchased', + insert_id: mainEvent.insert_id ? `${mainEvent.insert_id}-${events.length + 1}` : undefined + }) + } + } + + const json: JSON = { + api_key: settings.apiKey, + events: events.map(removeUndefined), + ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}) + } + + const endpoint = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) + + return request(endpoint, { + method: 'post', + json + }) } -export function getUserProperties(payload: Payload): UserProperties { +function getUserProperties(payload: LogEventV2Payload | PurchasePayload): UserProperties { const { - setOnce, - setAlways, - add, autocaptureAttributionEnabled, autocaptureAttributionSet, autocaptureAttributionSetOnce, @@ -18,6 +84,31 @@ export function getUserProperties(payload: Payload): UserProperties { user_properties } = payload + let setOnce: UserProperties['$setOnce'] = {} + let setAlways: UserProperties['$set'] = {} + let add: UserProperties['$add'] = {} + + if ('utm_properties' in payload || 'referrer' in payload) { + // For LogPurchase and LogEvent Actions + const { utm_properties, referrer } = payload + setAlways = { + ...(referrer ? { referrer } : {}), + ...(utm_properties || {}) + } + setOnce = { + ...(referrer ? { initial_referrer: referrer } : {}), + ...(utm_properties + ? Object.fromEntries(Object.entries(utm_properties).map(([k, v]) => [`initial_${k}`, v])) + : {}) + } + } + else if ('setOnce' in payload || 'setAlways' in payload || 'add' in payload){ + // For LogEventV2 Action + setOnce = payload.setOnce as UserProperties['$setOnce'] + setAlways = payload.setAlways as UserProperties['$set'] + add = payload.add as UserProperties['$add'] + } + if (autocaptureAttributionEnabled) { // If autocapture attribution is enabled, we need to make sure that attribution keys are not sent from the setAlways and setOnce fields for (const key of AMPLITUDE_ATTRIBUTION_KEYS) { @@ -39,17 +130,31 @@ export function getUserProperties(payload: Payload): UserProperties { ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }} : {}), ...(compact(add) ? { $add: add as { [k: string]: string } } : {}), - ...(compact(autocaptureAttributionUnset) ? { $unset: autocaptureAttributionUnset as { [k: string]: string } } : {}), + ...(compact(autocaptureAttributionEnabled ? autocaptureAttributionUnset : {}) + ? { $unset: autocaptureAttributionEnabled ? autocaptureAttributionUnset as { [k: string]: string } : {} as { [k: string]: string } } + : {}) } - return userProperties } +function getRevenueProperties(payload: EventRevenue): EventRevenue { + let { revenue } = payload + const { quantity, price, revenueType, productId } = payload + if (typeof quantity === 'number' && typeof price === 'number') { + revenue = quantity * price + } + if (!revenue) { + return {} + } + return { + revenue, + revenueType: revenueType ?? 'Purchase', + quantity: typeof quantity === 'number' ? Math.round(quantity) : undefined, + price: price, + productId + } +} - - - - - - - \ No newline at end of file +function compact(object: { [k: string]: unknown } | undefined): boolean { + return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts index e09b8ad3529..30357aeb766 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts @@ -1,4 +1,4 @@ const REVENUE_KETS = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] -const USER_PROPERTY_KEYS = ['setOnce', 'setAlways','add','autocaptureAttributionEnabled','autocaptureAttributionSet','autocaptureAttributionSetOnce','autocaptureAttributionUnset'] -export const KEYS_TO_OMIT = [...REVENUE_KETS, ...USER_PROPERTY_KEYS] +const USER_PROPERTY_KEYS = ['utm_properties', 'referrer', 'setOnce', 'setAlways','add','autocaptureAttributionEnabled','autocaptureAttributionSet','autocaptureAttributionSetOnce','autocaptureAttributionUnset'] +export const KEYS_TO_OMIT = [...REVENUE_KETS, ...USER_PROPERTY_KEYS, 'trackRevenuePerProduct'] export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 8abe18a6831..f17c300de01 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,16 +1,10 @@ -import { ActionDefinition, omit, removeUndefined } from '@segment/actions-core' -import dayjs from 'dayjs' +import { ActionDefinition} from '@segment/actions-core' import { eventSchema } from '../event-schema' import type { Settings } from '../generated-types' -import { getEndpointByRegion } from '../regional-endpoints' -import { parseUserAgentProperties } from '../user-agent' import type { Payload } from './generated-types' -import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' import { autocaptureFields } from './autocapture-fields' -import { getUserProperties } from './autocapture-attribution' -import { AmplitudeEvent, JSON } from './types' -import { KEYS_TO_OMIT } from './constants' +import { send } from './autocapture-attribution' const action: ActionDefinition = { title: 'Log Event V2', @@ -207,48 +201,7 @@ const action: ActionDefinition = { userAgentData }, perform: (request, { payload, settings }) => { - const { - time, - session_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - min_id_length, - platform, - library, - user_id, - ...rest - } = omit(payload, KEYS_TO_OMIT) - - const user_properties = getUserProperties(payload) - - const event: AmplitudeEvent = { - ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), - ...(includeRawUserAgent && { user_agent: userAgent }), - ...rest, - ...{ user_id: user_id || null }, - ...(platform ? { platform: platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } : {}), - ...(library === 'analytics.js' && !platform ? { platform: 'Web' } : {}), - ...(time && dayjs.utc(time).isValid() ? { time: dayjs.utc(time).valueOf() } : {}), - ...(session_id && dayjs.utc(session_id).isValid() ? { session_id: formatSessionId(session_id) } : {}), - ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}), - ...(user_properties ? { user_properties } : {}), - library: 'segment' - } - - const json: JSON = { - api_key: settings.apiKey, - events: [removeUndefined(event)], - ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}) - } - - const endpoint = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) - - return request(endpoint, { - method: 'post', - json - }) + return send(request, payload, settings, false) } } diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts index 4ee6465e1ee..d23c3db28d5 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts @@ -1,6 +1,6 @@ import { ParsedUA } from '../user-agent' -export interface AmplitudeEvent { +export interface AmplitudeEvent extends EventRevenue { user_id?: string | null device_id?: string event_type: string @@ -22,11 +22,6 @@ export interface AmplitudeEvent { city?: string dma?: string language?: string - price?: number - quantity?: number - revenue?: number - productId?: string - revenueType?: string location_lat?: number location_lng?: number ip?: string @@ -46,11 +41,19 @@ export interface AmplitudeEvent { } } +export interface EventRevenue { + revenue?: number + price?: number + productId?: string + quantity?: number + revenueType?: string +} + export interface UserProperties { - $set?: { [k: string]: string } - $setOnce?: { [k: string]: string } - $unset?: { [k: string]: string } - $add?: { [k: string]: string } + $set?: Record + $setOnce?: Record<`initial_${string}`, string> + $unset?: Record + $add?: Record [k: string]: unknown } diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts index acc0a64d7c5..4ee181286f7 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts @@ -182,21 +182,27 @@ export interface Payload { [k: string]: unknown }[] /** - * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - use_batch_endpoint?: boolean + autocaptureAttributionEnabled?: boolean /** - * The user agent of the device sending the event. + * Utility field used to detect if any attribution values need to be set. */ - userAgent?: string + autocaptureAttributionSet?: { + [k: string]: unknown + } /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + * Utility field used to detect if any attribution values need to be set_once. */ - userAgentParsing?: boolean + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * Utility field used to detect if any attribution values need to be unset. */ - includeRawUserAgent?: boolean + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** * UTM Tracking Properties */ @@ -211,6 +217,22 @@ export interface Payload { * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” */ referrer?: string + /** + * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). + */ + use_batch_endpoint?: boolean + /** + * The user agent of the device sending the event. + */ + userAgent?: string + /** + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + */ + userAgentParsing?: boolean + /** + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + */ + includeRawUserAgent?: boolean /** * Amplitude has a default minimum id lenght of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index 7db7dc8889e..4eb55fd34a5 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -1,54 +1,10 @@ -import { omit, removeUndefined } from '@segment/actions-core' -import dayjs from '../../../lib/dayjs' import { eventSchema } from '../event-schema' import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { convertUTMProperties } from '../utm' -import { convertReferrerProperty } from '../referrer' -import { mergeUserProperties } from '../merge-user-properties' -import { parseUserAgentProperties } from '../user-agent' -import { getEndpointByRegion } from '../regional-endpoints' -import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' - -export interface AmplitudeEvent extends Omit { - library?: string - time?: number - session_id?: number - options?: { - min_id_length: number - } -} - -const revenueKeys = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] - -interface EventRevenue { - revenue?: number - price?: number - productId?: string - quantity?: number - revenueType?: string -} - -function getRevenueProperties(payload: EventRevenue): EventRevenue { - let revenue = payload.revenue - if (typeof payload.quantity === 'number' && typeof payload.price === 'number') { - revenue = payload.quantity * payload.price - } - - if (!revenue) { - return {} - } - - return { - revenue, - revenueType: payload.revenueType ?? 'Purchase', - quantity: typeof payload.quantity === 'number' ? Math.round(payload.quantity) : undefined, - price: payload.price, - productId: payload.productId - } -} +import { autocaptureFields } from '../logEventV2/autocapture-fields' +import { send } from '../logEventV2/autocapture-attribution' const action: ActionDefinition = { title: 'Log Purchase', @@ -138,35 +94,7 @@ const action: ActionDefinition = { ] } }, - use_batch_endpoint: { - label: 'Use Batch Endpoint', - description: - "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", - type: 'boolean', - default: false - }, - userAgent: { - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } - }, - userAgentParsing: { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', - default: true - }, - includeRawUserAgent: { - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false - }, + ...autocaptureFields, utm_properties: { label: 'UTM Properties', type: 'object', @@ -210,6 +138,35 @@ const action: ActionDefinition = { '@path': '$.context.page.referrer' } }, + use_batch_endpoint: { + label: 'Use Batch Endpoint', + description: + "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", + type: 'boolean', + default: false + }, + userAgent: { + label: 'User Agent', + type: 'string', + description: 'The user agent of the device sending the event.', + default: { + '@path': '$.context.userAgent' + } + }, + userAgentParsing: { + label: 'User Agent Parsing', + type: 'boolean', + description: + 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', + default: true + }, + includeRawUserAgent: { + label: 'Include Raw User Agent', + type: 'boolean', + description: + 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', + default: false + }, min_id_length: { label: 'Minimum ID Length', description: @@ -220,89 +177,13 @@ const action: ActionDefinition = { userAgentData }, perform: (request, { payload, settings }) => { - // Omit revenue properties initially because we will manually stitch those into events as prescribed - const { - products = [], - trackRevenuePerProduct, - time, - session_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - utm_properties, - referrer, - min_id_length, - library, - ...rest - } = omit(payload, revenueKeys) - const properties = rest as AmplitudeEvent - let options - - if (properties.platform) { - properties.platform = properties.platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') - } - - if (library === 'analytics.js' && !properties.platform) { - properties.platform = 'Web' - } - - if (time && dayjs.utc(time).isValid()) { - properties.time = dayjs.utc(time).valueOf() - } - - if (session_id && dayjs.utc(session_id).isValid()) { - properties.session_id = formatSessionId(session_id) - } - - if (Object.keys(payload.utm_properties ?? {}).length || payload.referrer) { - properties.user_properties = mergeUserProperties( - convertUTMProperties({ utm_properties }), - convertReferrerProperty({ referrer }), - omit(properties.user_properties ?? {}, ['utm_properties', 'referrer']) - ) - } + return send(request, payload, settings, true) + } +} - if (min_id_length && min_id_length > 0) { - options = { min_id_length } - } +export default action - const events: AmplitudeEvent[] = [ - { - // Conditionally parse user agent using amplitude's library - ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), - ...(includeRawUserAgent && { user_agent: userAgent }), - // Make sure any top-level properties take precedence over user-agent properties - ...removeUndefined(properties), - // Conditionally track revenue with main event - ...(products.length && trackRevenuePerProduct ? {} : getRevenueProperties(payload)), - library: 'segment' - } - ] - for (const product of products) { - events.push({ - ...properties, - // Or track revenue per product - ...(trackRevenuePerProduct ? getRevenueProperties(product as EventRevenue) : {}), - event_properties: product, - event_type: 'Product Purchased', - insert_id: properties.insert_id ? `${properties.insert_id}-${events.length + 1}` : undefined, - library: 'segment' - }) - } - const endpoint = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) - return request(endpoint, { - method: 'post', - json: { - api_key: settings.apiKey, - events, - options - } - }) - } -} -export default action From 1622d9a842db14bbd5fa1acab424272ba2c340fa Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 28 Nov 2025 11:06:48 +0000 Subject: [PATCH 29/38] converting logevent action --- .../amplitude/__tests__/amplitude.test.ts | 43 ++---- .../destinations/amplitude/logEvent/index.ts | 146 ++++-------------- .../logEventV2/autocapture-attribution.ts | 5 +- 3 files changed, 53 insertions(+), 141 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts index 0d45146c848..610333de36e 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts @@ -1079,9 +1079,7 @@ describe('Amplitude', () => { "events": Array [ Object { "device_id": "6fd32a7e-3c56-44c2-bd32-62bbec44c53d", - "device_manufacturer": undefined, "device_model": "Mac OS", - "device_type": undefined, "event_properties": Object {}, "event_type": "Test Event", "library": "segment", @@ -1093,7 +1091,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1147,7 +1144,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1201,7 +1197,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1296,9 +1291,7 @@ describe('Amplitude', () => { "events": Array [ Object { "device_id": "6fd32a7e-3c56-44c2-bd32-62bbec44c53d", - "device_manufacturer": undefined, "device_model": "Mac OS", - "device_type": undefined, "event_properties": Object {}, "event_type": "Test Event", "library": "segment", @@ -1310,7 +1303,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1930,24 +1922,23 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.json).toMatchInlineSnapshot(` - Object { - "api_key": undefined, - "events": Array [ - Object { - "device_id": "foo", - "event_properties": Object {}, - "event_type": "Test Event", - "idfv": "foo", - "library": "segment", - "time": 1618245157710, - "use_batch_endpoint": false, - "user_id": "user1234", - "user_properties": Object {}, - }, - ], - "options": undefined, - } - `) + Object { + "api_key": undefined, + "events": Array [ + Object { + "device_id": "foo", + "event_properties": Object {}, + "event_type": "Test Event", + "idfv": "foo", + "library": "segment", + "time": 1618245157710, + "use_batch_endpoint": false, + "user_id": "user1234", + "user_properties": Object {}, + }, + ], + } + `) }) describe('identifyUser', () => { diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index aa10e91d9d6..7632cfb04b2 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -1,27 +1,11 @@ -import { omit, removeUndefined } from '@segment/actions-core' -import dayjs from '../../../lib/dayjs' import { eventSchema } from '../event-schema' import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { convertUTMProperties } from '../utm' -import { convertReferrerProperty } from '../referrer' -import { mergeUserProperties } from '../merge-user-properties' -import { parseUserAgentProperties } from '../user-agent' -import { getEndpointByRegion } from '../regional-endpoints' -import { formatSessionId } from '../convert-timestamp' import { userAgentData } from '../properties' +import { autocaptureFields } from '../logEventV2/autocapture-fields' +import { send } from '../logEventV2/autocapture-attribution' -export interface AmplitudeEvent extends Omit { - library?: string - time?: number - session_id?: number - options?: { - min_id_length: number - } -} - -const revenueKeys = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] const action: ActionDefinition = { title: 'Log Event', description: 'Send an event to Amplitude.', @@ -88,35 +72,7 @@ const action: ActionDefinition = { ] } }, - use_batch_endpoint: { - label: 'Use Batch Endpoint', - description: - "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", - type: 'boolean', - default: false - }, - userAgent: { - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } - }, - userAgentParsing: { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', - default: true - }, - includeRawUserAgent: { - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false - }, + ...autocaptureFields, utm_properties: { label: 'UTM Properties', type: 'object', @@ -160,6 +116,35 @@ const action: ActionDefinition = { '@path': '$.context.page.referrer' } }, + use_batch_endpoint: { + label: 'Use Batch Endpoint', + description: + "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", + type: 'boolean', + default: false + }, + userAgent: { + label: 'User Agent', + type: 'string', + description: 'The user agent of the device sending the event.', + default: { + '@path': '$.context.userAgent' + } + }, + userAgentParsing: { + label: 'User Agent Parsing', + type: 'boolean', + description: + 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', + default: true + }, + includeRawUserAgent: { + label: 'Include Raw User Agent', + type: 'boolean', + description: + 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', + default: false + }, min_id_length: { label: 'Minimum ID Length', description: @@ -170,72 +155,7 @@ const action: ActionDefinition = { userAgentData }, perform: (request, { payload, settings }) => { - // Omit revenue properties initially because we will manually stitch those into events as prescribed - const { - time, - session_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - utm_properties, - referrer, - min_id_length, - library, - ...rest - } = omit(payload, revenueKeys) - const properties = rest as AmplitudeEvent - let options - - if (properties.platform) { - properties.platform = properties.platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') - } - - if (library === 'analytics.js' && !properties.platform) { - properties.platform = 'Web' - } - - if (time && dayjs.utc(time).isValid()) { - properties.time = dayjs.utc(time).valueOf() - } - - if (session_id && dayjs.utc(session_id).isValid()) { - properties.session_id = formatSessionId(session_id) - } - - if (Object.keys(payload.utm_properties ?? {}).length || payload.referrer) { - properties.user_properties = mergeUserProperties( - convertUTMProperties({ utm_properties }), - convertReferrerProperty({ referrer }), - omit(properties.user_properties ?? {}, ['utm_properties', 'referrer']) - ) - } - - if (min_id_length && min_id_length > 0) { - options = { min_id_length } - } - - const events: AmplitudeEvent[] = [ - { - // Conditionally parse user agent using amplitude's library - ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), - ...(includeRawUserAgent && { user_agent: userAgent }), - // Make sure any top-level properties take precedence over user-agent properties - ...removeUndefined(properties), - library: 'segment' - } - ] - - const endpoint = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) - - return request(endpoint, { - method: 'post', - json: { - api_key: settings.apiKey, - events, - options - } - }) + return send(request, payload, settings, false) } } diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts index bb91a19030e..8044e830b84 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts @@ -1,4 +1,5 @@ import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' +import { Payload as LogEventPayload} from '../logEvent/generated-types' import { Payload as LogEventV2Payload} from './generated-types' import { Payload as PurchasePayload } from '../logPurchase/generated-types' import { UserProperties, EventRevenue, AmplitudeEvent, JSON } from './types' @@ -10,7 +11,7 @@ import { formatSessionId } from '../convert-timestamp' import { getEndpointByRegion } from '../regional-endpoints' import dayjs from '../../../lib/dayjs' -export function send(request: RequestClient, payload: LogEventV2Payload | PurchasePayload, settings: Settings, isPurchaseEvent: boolean) { +export function send(request: RequestClient, payload: LogEventPayload | LogEventV2Payload | PurchasePayload, settings: Settings, isPurchaseEvent: boolean) { const { time, session_id, @@ -75,7 +76,7 @@ export function send(request: RequestClient, payload: LogEventV2Payload | Purcha }) } -function getUserProperties(payload: LogEventV2Payload | PurchasePayload): UserProperties { +function getUserProperties(payload: LogEventPayload | LogEventV2Payload | PurchasePayload): UserProperties { const { autocaptureAttributionEnabled, autocaptureAttributionSet, From 671f5ab2326779fccce3971f86a653974ec73d1b Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 28 Nov 2025 16:02:11 +0000 Subject: [PATCH 30/38] what am I doing? --- .../__tests__/convert-timestamp.test.ts | 2 +- .../__tests__/merge-user-properties.test.ts | 30 -- .../amplitude/__tests__/referrer.test.ts | 49 -- .../amplitude/__tests__/user-agent.test.ts | 2 +- .../amplitude/__tests__/utm.test.ts | 68 --- .../{logEventV2 => }/autocapture-fields.ts | 3 +- .../amplitude/common-functions.ts | 139 +++++ .../amplitude/convert-timestamp.ts | 12 - .../destinations/amplitude/event-schema.ts | 309 ----------- .../constants.ts => events-constants.ts} | 0 .../amplitude/events-functions.ts | 109 ++++ .../src/destinations/amplitude/fields.ts | 480 ++++++++++++++++++ .../amplitude/groupIdentifyUser/index.ts | 52 +- .../amplitude/identifyUser/constants.ts | 1 + .../amplitude/identifyUser/functions.ts | 47 ++ .../amplitude/identifyUser/generated-types.ts | 22 + .../amplitude/identifyUser/index.ts | 344 +++---------- .../amplitude/identifyUser/types.ts | 23 + .../src/destinations/amplitude/index.ts | 2 +- .../amplitude/logEvent/generated-types.ts | 38 +- .../destinations/amplitude/logEvent/index.ts | 4 +- .../logEventV2/autocapture-attribution.ts | 161 ------ .../amplitude/logEventV2/index.ts | 4 +- .../amplitude/logPurchase/index.ts | 4 +- .../destinations/amplitude/mapUser/index.ts | 2 +- .../amplitude/merge-user-properties.ts | 18 - .../src/destinations/amplitude/properties.ts | 21 - .../src/destinations/amplitude/referrer.ts | 21 - .../amplitude/regional-endpoints.ts | 46 -- .../amplitude/{logEventV2 => }/types.ts | 33 +- .../src/destinations/amplitude/user-agent.ts | 37 -- .../src/destinations/amplitude/utm.ts | 47 -- 32 files changed, 953 insertions(+), 1177 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/amplitude/__tests__/merge-user-properties.test.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/__tests__/referrer.test.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/__tests__/utm.test.ts rename packages/destination-actions/src/destinations/amplitude/{logEventV2 => }/autocapture-fields.ts (95%) create mode 100644 packages/destination-actions/src/destinations/amplitude/common-functions.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/convert-timestamp.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/event-schema.ts rename packages/destination-actions/src/destinations/amplitude/{logEventV2/constants.ts => events-constants.ts} (100%) create mode 100644 packages/destination-actions/src/destinations/amplitude/events-functions.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/identifyUser/constants.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/identifyUser/functions.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/identifyUser/types.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/merge-user-properties.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/properties.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/referrer.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/regional-endpoints.ts rename packages/destination-actions/src/destinations/amplitude/{logEventV2 => }/types.ts (77%) delete mode 100644 packages/destination-actions/src/destinations/amplitude/user-agent.ts delete mode 100644 packages/destination-actions/src/destinations/amplitude/utm.ts diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/convert-timestamp.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/convert-timestamp.test.ts index dd4516eef8a..cd9b0acd87b 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/convert-timestamp.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/convert-timestamp.test.ts @@ -1,4 +1,4 @@ -import { formatSessionId } from '../convert-timestamp' +import { formatSessionId } from '../events-functions' describe('Amplitude - Convert timestamp - format session_id', () => { it('should convert string to number', () => { diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/merge-user-properties.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/merge-user-properties.test.ts deleted file mode 100644 index 8aadd7722e8..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/merge-user-properties.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { mergeUserProperties } from '../merge-user-properties' - -describe('Amplitude - Merge user properties', () => { - it('should work without crashing', () => { - const result = mergeUserProperties({ a: 1 }) - expect(result).toEqual({ a: 1 }) - }) - - it('should merge two first level props', () => { - const a = { a: 1 } - const b = { b: 'two' } - const result = mergeUserProperties(a, b) - expect(result).toEqual({ a: 1, b: 'two' }) - }) - - it('should support set and setOnce explicitly', () => { - const a = { a: 1, $set: { a: 1 }, $setOnce: { aa: 11 } } - const b = { b: 'two', $set: { b: 'two' }, $setOnce: { bb: 'twotwo' } } - const result = mergeUserProperties(a, b) - expect(result).toEqual({ a: 1, b: 'two', $set: { a: 1, b: 'two' }, $setOnce: { aa: 11, bb: 'twotwo' } }) - }) - - it('should support merging existing flat props', () => { - const a = { $set: { a: 1 }, $setOnce: { aa: 11 } } - const b = { $set: { b: 'two' }, $setOnce: { bb: 'twotwo' } } - const c = { a: 1, b: 2 } - const result = mergeUserProperties(a, b, c) - expect(result).toEqual({ a: 1, b: 2, $set: { a: 1, b: 'two' }, $setOnce: { aa: 11, bb: 'twotwo' } }) - }) -}) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/referrer.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/referrer.test.ts deleted file mode 100644 index 2e9babd4cc2..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/referrer.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { convertReferrerProperty } from '../referrer' - -describe('Amplitude - referrer utility', () => { - it('should run without exploding', () => { - const result = convertReferrerProperty({}) - expect(result).toEqual({}) - }) - - it('should append $set and $setOnce when referrer is provided and user_properties exists', () => { - const user_properties = { - a: 1, - b: 'two', - c: { - d: true - } - } - - const referrer = 'some ref' - - const payload = { - user_properties, - referrer - } - - const result = convertReferrerProperty(payload) - expect(result).toEqual({ - $set: { - referrer - }, - $setOnce: { - initial_referrer: referrer - } - }) - }) - - it('should create a user_properties when referrer is provided and there is not an existing', () => { - const referrer = 'some ref 2' - - const result = convertReferrerProperty({ referrer }) - expect(result).toEqual({ - $set: { - referrer - }, - $setOnce: { - initial_referrer: referrer - } - }) - }) -}) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/user-agent.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/user-agent.test.ts index 0af71526c5b..85e3184f6cc 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/user-agent.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/user-agent.test.ts @@ -1,4 +1,4 @@ -import { parseUserAgentProperties } from '../user-agent' +import { parseUserAgentProperties } from '../common-functions' describe('amplitude - custom user agent parsing', () => { it('should parse custom user agent', () => { diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/utm.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/utm.test.ts deleted file mode 100644 index 305bd643d9e..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/utm.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { convertUTMProperties } from '../utm' - -describe('Amplitude - utm utility', () => { - it('should run without exploding', () => { - const result = convertUTMProperties({}) - expect(result).toEqual({}) - }) - - it('should append $set and $setOnce when utm is provided and user_properties exists', () => { - const user_properties = { - a: 1, - b: 'two', - c: { - d: true - } - } - - const utm_properties = { - utm_source: 'source', - utm_medium: 'medium', - utm_campaign: 'campaign', - utm_term: 'term', - utm_content: 'content' - } - - const payload = { - user_properties, - utm_properties - } - - const result = convertUTMProperties(payload) - expect(result).toEqual({ - $set: { - ...utm_properties - }, - $setOnce: { - initial_utm_source: 'source', - initial_utm_medium: 'medium', - initial_utm_campaign: 'campaign', - initial_utm_term: 'term', - initial_utm_content: 'content' - } - }) - }) - - it('should create a user_properties when utm is provided and there is not an existing', () => { - const utm_properties = { - utm_source: 'source', - utm_medium: 'medium', - utm_campaign: 'campaign', - utm_term: 'term', - utm_content: 'content' - } - const result = convertUTMProperties({ utm_properties }) - expect(result).toEqual({ - $set: { - ...utm_properties - }, - $setOnce: { - initial_utm_source: 'source', - initial_utm_medium: 'medium', - initial_utm_campaign: 'campaign', - initial_utm_term: 'term', - initial_utm_content: 'content' - } - }) - }) -}) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts similarity index 95% rename from packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts rename to packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts index 9e69d757a28..3d3f6c2dec3 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts @@ -1,6 +1,5 @@ import type { InputField } from '@segment/actions-core' -import { DESTINATION_INTEGRATION_NAME } from './constants' - +import { DESTINATION_INTEGRATION_NAME } from './events-constants' export const autocaptureFields: Record = { autocaptureAttributionEnabled: { label: 'Autocapture Attribution Enabled', diff --git a/packages/destination-actions/src/destinations/amplitude/common-functions.ts b/packages/destination-actions/src/destinations/amplitude/common-functions.ts new file mode 100644 index 00000000000..d1f3912acb7 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/common-functions.ts @@ -0,0 +1,139 @@ +import UaParser from '@amplitude/ua-parser-js' +import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' +import { Payload as LogEventPayload} from './logEvent/generated-types' +import { Payload as LogEventV2Payload} from './logEventV2/generated-types' +import { Payload as PurchasePayload } from './logPurchase/generated-types' +import { Payload as IdentifyUserPayload} from './identifyUser/generated-types' +import { UserProperties, UserAgentData, ParsedUA, Region } from './types' + +export function getUserProperties(payload: LogEventPayload | LogEventV2Payload | PurchasePayload | IdentifyUserPayload): UserProperties { + const { + autocaptureAttributionEnabled, + autocaptureAttributionSet, + autocaptureAttributionSetOnce, + autocaptureAttributionUnset, + user_properties + } = payload + + let setOnce: UserProperties['$setOnce'] = {} + let setAlways: UserProperties['$set'] = {} + let add: UserProperties['$add'] = {} + + if ('utm_properties' in payload || 'referrer' in payload) { + // For LogPurchase and LogEvent Actions + const { utm_properties, referrer } = payload + setAlways = { + ...(referrer ? { referrer } : {}), + ...(utm_properties || {}) + } + setOnce = { + ...(referrer ? { initial_referrer: referrer } : {}), + ...(utm_properties + ? Object.fromEntries(Object.entries(utm_properties).map(([k, v]) => [`initial_${k}`, v])) + : {}) + } + } + else if ('setOnce' in payload || 'setAlways' in payload || 'add' in payload){ + // For LogEventV2 Action + setOnce = payload.setOnce as UserProperties['$setOnce'] + setAlways = payload.setAlways as UserProperties['$set'] + add = payload.add as UserProperties['$add'] + } + + if (autocaptureAttributionEnabled) { + // If autocapture attribution is enabled, we need to make sure that attribution keys are not sent from the setAlways and setOnce fields + for (const key of AMPLITUDE_ATTRIBUTION_KEYS) { + if( typeof setAlways === "object" && setAlways !== null){ + delete setAlways[key] + } + if(typeof setOnce === "object" && setOnce !== null){ + delete setOnce[`initial_${key}`] + } + } + } + + const userProperties = { + ...user_properties, + ...(compact(autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } as { [k: string]: string } : setOnce as { [k: string]: string }) + ? { $setOnce: autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } as { [k: string]: string }: setOnce as { [k: string]: string }} + : {}), + ...(compact(autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }) + ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }} + : {}), + ...(compact(add) ? { $add: add as { [k: string]: string } } : {}), + ...(compact(autocaptureAttributionEnabled ? autocaptureAttributionUnset as { [k: string]: string } : {}) + ? { $unset: autocaptureAttributionEnabled ? autocaptureAttributionUnset as { [k: string]: string } : {} as { [k: string]: string } } + : {}) + } + return userProperties +} + +function compact(object: { [k: string]: unknown } | undefined): boolean { + return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 +} + +export const endpoints = { + batch: { + north_america: 'https://api2.amplitude.com/batch', + europe: 'https://api.eu.amplitude.com/batch' + }, + deletions: { + north_america: 'https://amplitude.com/api/2/deletions/users', + europe: 'https://analytics.eu.amplitude.com/api/2/deletions/users' + }, + httpapi: { + north_america: 'https://api2.amplitude.com/2/httpapi', + europe: 'https://api.eu.amplitude.com/2/httpapi' + }, + identify: { + north_america: 'https://api2.amplitude.com/identify', + europe: 'https://api.eu.amplitude.com/identify' + }, + groupidentify: { + north_america: 'https://api2.amplitude.com/groupidentify', + europe: 'https://api.eu.amplitude.com/groupidentify' + }, + usermap: { + north_america: 'https://api.amplitude.com/usermap', + europe: 'https://api.eu.amplitude.com/usermap' + }, + usersearch: { + north_america: 'https://amplitude.com/api/2/usersearch', + europe: 'https://analytics.eu.amplitude.com/api/2/usersearch' + } +} + +/** + * Retrieves Amplitude API endpoints for a given region. If the region + * provided does not exist, the region defaults to 'north_america'. + * + * @param endpoint name of the API endpoint + * @param region data residency region + * @returns regional API endpoint + */ +export function getEndpointByRegion(endpoint: keyof typeof endpoints, region?: string): string { + return endpoints[endpoint][region as Region] ?? endpoints[endpoint]['north_america'] +} + +export function parseUserAgentProperties(userAgent?: string, userAgentData?: UserAgentData): ParsedUA { + if (!userAgent) { + return {} + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const parser = new UaParser(userAgent) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const device = parser.getDevice() + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const os = parser.getOS() + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const browser = parser.getBrowser() + + return { + os_name: os.name ?? browser.name, + os_version: userAgentData?.platformVersion ?? browser.major, + device_manufacturer: device.vendor, + device_model: userAgentData?.model ?? device.model ?? os.name, + device_type: device.type + } +} diff --git a/packages/destination-actions/src/destinations/amplitude/convert-timestamp.ts b/packages/destination-actions/src/destinations/amplitude/convert-timestamp.ts deleted file mode 100644 index d4bfb3b2d17..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/convert-timestamp.ts +++ /dev/null @@ -1,12 +0,0 @@ -import dayjs from '../../lib/dayjs' - -export function formatSessionId(session_id: string | number): number { - // Timestamps may be on a `string` field, so check if the string is only - // numbers. If it is, convert it into a Number since it's probably already a unix timestamp. - // DayJS doesn't parse unix timestamps correctly outside of the `.unix()` - // initializer. - if (typeof session_id === 'string' && /^\d+$/.test(session_id)) { - return Number(session_id) - } - return dayjs.utc(session_id).valueOf() -} diff --git a/packages/destination-actions/src/destinations/amplitude/event-schema.ts b/packages/destination-actions/src/destinations/amplitude/event-schema.ts deleted file mode 100644 index 2d2c8fac566..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/event-schema.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { InputField } from '@segment/actions-core' - -/** - * The common fields defined by Amplitude's events api - * @see {@link https://developers.amplitude.com/docs/http-api-v2#keys-for-the-event-argument} - */ -export const eventSchema: Record = { - user_id: { - label: 'User ID', - type: 'string', - allowNull: true, - description: - 'A readable ID specified by you. Must have a minimum length of 5 characters. Required unless device ID is present. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event.', - default: { - '@path': '$.userId' - } - }, - device_id: { - label: 'Device ID', - type: 'string', - description: - 'A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID.', - default: { - '@if': { - exists: { '@path': '$.context.device.id' }, - then: { '@path': '$.context.device.id' }, - else: { '@path': '$.anonymousId' } - } - } - }, - event_type: { - label: 'Event Type', - type: 'string', - description: 'A unique identifier for your event.', - required: true, - default: { - '@path': '$.event' - } - }, - session_id: { - label: 'Session ID', - type: 'datetime', - description: - 'The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source.', - default: { - '@path': '$.integrations.Actions Amplitude.session_id' - } - }, - time: { - label: 'Timestamp', - type: 'datetime', - description: - 'The timestamp of the event. If time is not sent with the event, it will be set to the request upload time.', - default: { - '@path': '$.timestamp' - } - }, - event_properties: { - label: 'Event Properties', - type: 'object', - description: - 'An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', - default: { - '@path': '$.properties' - } - }, - user_properties: { - label: 'User Properties', - type: 'object', - description: - 'An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', - default: { - '@path': '$.traits' - } - }, - groups: { - label: 'Groups', - type: 'object', - description: - 'Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on.' - }, - app_version: { - label: 'App Version', - type: 'string', - description: 'The current version of your application.', - default: { - '@path': '$.context.app.version' - } - }, - platform: { - label: 'Platform', - type: 'string', - description: - 'Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent.', - default: { - '@path': '$.context.device.type' - } - }, - os_name: { - label: 'OS Name', - type: 'string', - description: 'The name of the mobile operating system or browser that the user is using.', - default: { - '@path': '$.context.os.name' - } - }, - os_version: { - label: 'OS Version', - type: 'string', - description: 'The version of the mobile operating system or browser the user is using.', - default: { - '@path': '$.context.os.version' - } - }, - device_brand: { - label: 'Device Brand', - type: 'string', - description: 'The device brand that the user is using.', - default: { - '@path': '$.context.device.brand' - } - }, - device_manufacturer: { - label: 'Device Manufacturer', - type: 'string', - description: 'The device manufacturer that the user is using.', - default: { - '@path': '$.context.device.manufacturer' - } - }, - device_model: { - label: 'Device Model', - type: 'string', - description: 'The device model that the user is using.', - default: { - '@path': '$.context.device.model' - } - }, - carrier: { - label: 'Carrier', - type: 'string', - description: 'The carrier that the user is using.', - default: { - '@path': '$.context.network.carrier' - } - }, - country: { - label: 'Country', - type: 'string', - description: 'The current country of the user.', - default: { - '@path': '$.context.location.country' - } - }, - region: { - label: 'Region', - type: 'string', - description: 'The current region of the user.', - default: { - '@path': '$.context.location.region' - } - }, - city: { - label: 'City', - type: 'string', - description: 'The current city of the user.', - default: { - '@path': '$.context.location.city' - } - }, - dma: { - label: 'Designated Market Area', - type: 'string', - description: 'The current Designated Market Area of the user.' - }, - language: { - label: 'Language', - type: 'string', - description: 'The language set by the user.', - default: { - '@path': '$.context.locale' - } - }, - price: { - label: 'Price', - type: 'number', - description: - 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.', - default: { - '@path': '$.properties.price' - } - }, - quantity: { - label: 'Quantity', - type: 'integer', - description: 'The quantity of the item purchased. Defaults to 1 if not specified.', - default: { - '@path': '$.properties.quantity' - } - }, - revenue: { - label: 'Revenue', - type: 'number', - description: - 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode.', - default: { - '@path': '$.properties.revenue' - } - }, - productId: { - label: 'Product ID', - type: 'string', - description: 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.', - default: { - '@path': '$.properties.productId' - } - }, - revenueType: { - label: 'Revenue Type', - type: 'string', - description: - 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.', - default: { - '@path': '$.properties.revenueType' - } - }, - location_lat: { - label: 'Latitude', - type: 'number', - description: 'The current Latitude of the user.', - default: { - '@path': '$.context.location.latitude' - } - }, - location_lng: { - label: 'Longtitude', - type: 'number', - description: 'The current Longitude of the user.', - default: { - '@path': '$.context.location.longitude' - } - }, - ip: { - label: 'IP Address', - type: 'string', - description: - 'The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user\'s location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. You can submit a request to Amplitude\'s platform specialist team here to configure this for you.', - default: { - '@path': '$.context.ip' - } - }, - idfa: { - label: 'Identifier For Advertiser (IDFA)', - type: 'string', - description: 'Identifier for Advertiser. _(iOS)_', - default: { - '@if': { - exists: { '@path': '$.context.device.advertisingId' }, - then: { '@path': '$.context.device.advertisingId' }, - else: { '@path': '$.context.device.idfa' } - } - } - }, - idfv: { - label: 'Identifier For Vendor (IDFV)', - type: 'string', - description: 'Identifier for Vendor. _(iOS)_', - default: { - '@path': '$.context.device.id' - } - }, - adid: { - label: 'Google Play Services Advertising ID', - type: 'string', - description: 'Google Play Services advertising ID. _(Android)_', - default: { - '@if': { - exists: { '@path': '$.context.device.advertisingId' }, - then: { '@path': '$.context.device.advertisingId' }, - else: { '@path': '$.context.device.idfa' } - } - } - }, - android_id: { - label: 'Android ID', - type: 'string', - description: 'Android ID (not the advertising ID). _(Android)_' - }, - event_id: { - label: 'Event ID', - type: 'integer', - description: - 'An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously.' - }, - insert_id: { - label: 'Insert ID', - type: 'string', - description: - 'Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time.' - }, - library: { - label: 'Library', - type: 'string', - description: 'The name of the library that generated the event.', - default: { - '@path': '$.context.library.name' - } - } -} diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts b/packages/destination-actions/src/destinations/amplitude/events-constants.ts similarity index 100% rename from packages/destination-actions/src/destinations/amplitude/logEventV2/constants.ts rename to packages/destination-actions/src/destinations/amplitude/events-constants.ts diff --git a/packages/destination-actions/src/destinations/amplitude/events-functions.ts b/packages/destination-actions/src/destinations/amplitude/events-functions.ts new file mode 100644 index 00000000000..b5fc5018858 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/events-functions.ts @@ -0,0 +1,109 @@ +import { Payload as LogEventPayload} from './logEvent/generated-types' +import { Payload as LogEventV2Payload} from './logEventV2/generated-types' +import { Payload as PurchasePayload } from './logPurchase/generated-types' +import { EventRevenue, AmplitudeEventJSON, JSON_PAYLOAD } from './types' +import { RequestClient, omit, removeUndefined } from '@segment/actions-core' +import { Settings } from './generated-types' +import { KEYS_TO_OMIT } from './events-constants' +import { parseUserAgentProperties } from './common-functions' +import dayjs from '../../lib/dayjs' +import { getEndpointByRegion, getUserProperties } from './common-functions' + +export function send( + request: RequestClient, + payload: LogEventPayload | LogEventV2Payload | PurchasePayload, + settings: Settings, + isPurchaseEvent: boolean, + ) { + + const { + time, + session_id, + userAgent, + userAgentParsing, + includeRawUserAgent, + userAgentData, + min_id_length, + platform, + library, + user_id, + products = [], + ...rest + } = omit(payload, KEYS_TO_OMIT) + + let trackRevenuePerProduct = false + if ('trackRevenuePerProduct' in payload) { + trackRevenuePerProduct = payload.trackRevenuePerProduct || false + } + + const user_properties = getUserProperties(payload) + + const events: AmplitudeEventJSON[] = [{ + ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), + ...(includeRawUserAgent && { user_agent: userAgent }), + ...rest, + ...{ user_id: user_id || null }, + ...(platform ? { platform: platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } : {}), + ...(library === 'analytics.js' && !platform ? { platform: 'Web' } : {}), + ...(time && dayjs.utc(time).isValid() ? { time: dayjs.utc(time).valueOf() } : {}), + ...(session_id && dayjs.utc(session_id).isValid() ? { session_id: formatSessionId(session_id) } : {}), + ...(user_properties ? { user_properties } : {}), + ...(products.length && trackRevenuePerProduct ? {} : getRevenueProperties(payload)), + library: 'segment' + }] + + if(isPurchaseEvent){ + const mainEvent = events[0] + for (const product of products) { + events.push({ + ...mainEvent, + ...(trackRevenuePerProduct ? getRevenueProperties(product as EventRevenue) : {}), + event_properties: product, + event_type: 'Product Purchased', + insert_id: mainEvent.insert_id ? `${mainEvent.insert_id}-${events.length + 1}` : undefined + }) + } + } + + const json: JSON_PAYLOAD = { + api_key: settings.apiKey, + events: events.map(removeUndefined), + ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}) + } + + const url = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) + + return request(url, { + method: 'post', + json + }) +} + +function getRevenueProperties(payload: EventRevenue): EventRevenue { + let { revenue } = payload + const { quantity, price, revenueType, productId } = payload + if (typeof quantity === 'number' && typeof price === 'number') { + revenue = quantity * price + } + if (!revenue) { + return {} + } + return { + revenue, + revenueType: revenueType ?? 'Purchase', + quantity: typeof quantity === 'number' ? Math.round(quantity) : undefined, + price: price, + productId + } +} + +export function formatSessionId(session_id: string | number): number { + // Timestamps may be on a `string` field, so check if the string is only + // numbers. If it is, convert it into a Number since it's probably already a unix timestamp. + // DayJS doesn't parse unix timestamps correctly outside of the `.unix()` + // initializer. + if (typeof session_id === 'string' && /^\d+$/.test(session_id)) { + return Number(session_id) + } + return dayjs.utc(session_id).valueOf() +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/fields.ts b/packages/destination-actions/src/destinations/amplitude/fields.ts new file mode 100644 index 00000000000..4871dc104c4 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/fields.ts @@ -0,0 +1,480 @@ +import { InputField } from '@segment/actions-core' + +export const user_id: InputField = { + label: 'User ID', + type: 'string', + allowNull: true, + description: 'A readable ID specified by you. Must have a minimum length of 5 characters. Required unless device ID is present. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event.', + default: { + '@path': '$.userId' + } +} + +export const device_id: InputField = { + label: 'Device ID', + type: 'string', + description: 'A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID.', + default: { + '@if': { + exists: { '@path': '$.context.device.id' }, + then: { '@path': '$.context.device.id' }, + else: { '@path': '$.anonymousId' } + } + } +} + +export const event_type: InputField = { + label: 'Event Type', + type: 'string', + description: 'A unique identifier for your event.', + required: true, + default: { + '@path': '$.event' + } +} + +export const session_id: InputField = { + label: 'Session ID', + type: 'datetime', + description: 'The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source.', + default: { + '@path': '$.integrations.Actions Amplitude.session_id' + } +} + +export const time: InputField = { + label: 'Timestamp', + type: 'datetime', + description: 'The timestamp of the event. If time is not sent with the event, it will be set to the request upload time.', + default: { + '@path': '$.timestamp' + } +} + +export const event_properties: InputField = { + label: 'Event Properties', + type: 'object', + description: + 'An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', + default: { + '@path': '$.properties' + } +} + +export const user_properties: InputField = { + label: 'User Properties', + type: 'object', + description: + 'An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', + default: { + '@path': '$.traits' + } +} + +export const groups: InputField = { + label: 'Groups', + type: 'object', + description: + 'Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on.' +} + +export const app_version: InputField = { + label: 'App Version', + type: 'string', + description: 'The current version of your application.', + default: { + '@path': '$.context.app.version' + } +} + + +export const platform: InputField = { + label: 'Platform', + type: 'string', + description: + 'Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent.', + default: { + '@path': '$.context.device.type' + } +} + +export const os_name: InputField = { + label: 'OS Name', + type: 'string', + description: 'The name of the mobile operating system or browser that the user is using.', + default: { + '@path': '$.context.os.name' + } +} + + +export const os_version: InputField = { + label: 'OS Version', + type: 'string', + description: 'The version of the mobile operating system or browser the user is using.', + default: { + '@path': '$.context.os.version' + } +}; + +export const device_brand: InputField = { + label: 'Device Brand', + type: 'string', + description: 'The device brand that the user is using.', + default: { + '@path': '$.context.device.brand' + } +}; + +export const device_manufacturer: InputField = { + label: 'Device Manufacturer', + type: 'string', + description: 'The device manufacturer that the user is using.', + default: { + '@path': '$.context.device.manufacturer' + } +}; + +export const device_model: InputField = { + label: 'Device Model', + type: 'string', + description: 'The device model that the user is using.', + default: { + '@path': '$.context.device.model' + } +}; + +export const carrier: InputField = { + label: 'Carrier', + type: 'string', + description: 'The carrier that the user is using.', + default: { + '@path': '$.context.network.carrier' + } +}; + +export const country: InputField = { + label: 'Country', + type: 'string', + description: 'The current country of the user.', + default: { + '@path': '$.context.location.country' + } +} + +export const region: InputField = { + label: 'Region', + type: 'string', + description: 'The current region of the user.', + default: { + '@path': '$.context.location.region' + } +} + +export const city: InputField = { + label: 'City', + type: 'string', + description: 'The current city of the user.', + default: { + '@path': '$.context.location.city' + } +} + +export const dma: InputField = { + label: 'Designated Market Area', + type: 'string', + description: 'The current Designated Market Area of the user.' +} + +export const language: InputField = { + label: 'Language', + type: 'string', + description: 'The language set by the user.', + default: { + '@path': '$.context.locale' + } +} + +export const price: InputField = { + label: 'Price', + type: 'number', + description: + 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.', + default: { + '@path': '$.properties.price' + } +} + +export const quantity: InputField = { + label: 'Quantity', + type: 'integer', + description: 'The quantity of the item purchased. Defaults to 1 if not specified.', + default: { + '@path': '$.properties.quantity' + } +} + +export const revenue: InputField = { + label: 'Revenue', + type: 'number', + description: + 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode.', + default: { + '@path': '$.properties.revenue' + } +} + +export const productId: InputField = { + label: 'Product ID', + type: 'string', + description: + 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.', + default: { + '@path': '$.properties.productId' + } +} + +export const revenueType: InputField = { + label: 'Revenue Type', + type: 'string', + description: + 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.', + default: { + '@path': '$.properties.revenueType' + } +} + +export const location_lat: InputField = { + label: 'Latitude', + type: 'number', + description: 'The current Latitude of the user.', + default: { + '@path': '$.context.location.latitude' + } +} + +export const location_lng: InputField = { + label: 'Longtitude', + type: 'number', + description: 'The current Longitude of the user.', + default: { + '@path': '$.context.location.longitude' + } +} + +export const ip: InputField = { + label: 'IP Address', + type: 'string', + description: + 'The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user\'s location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers.', + default: { + '@path': '$.context.ip' + } +} + +export const idfa: InputField = { + label: 'Identifier For Advertiser (IDFA)', + type: 'string', + description: 'Identifier for Advertiser. _(iOS)_', + default: { + '@if': { + exists: { '@path': '$.context.device.advertisingId' }, + then: { '@path': '$.context.device.advertisingId' }, + else: { '@path': '$.context.device.idfa' } + } + } +} + +export const idfv: InputField = { + label: 'Identifier For Vendor (IDFV)', + type: 'string', + description: 'Identifier for Vendor. _(iOS)_', + default: { + '@path': '$.context.device.id' + } +} + +export const adid: InputField = { + label: 'Google Play Services Advertising ID', + type: 'string', + description: 'Google Play Services advertising ID. _(Android)_', + default: { + '@if': { + exists: { '@path': '$.context.device.advertisingId' }, + then: { '@path': '$.context.device.advertisingId' }, + else: { '@path': '$.context.device.idfa' } + } + } +} + +export const android_id: InputField = { + label: 'Android ID', + type: 'string', + description: 'Android ID (not the advertising ID). _(Android)_' +} + +export const event_id: InputField = { + label: 'Event ID', + type: 'integer', + description: + 'An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously.' +} + +export const insert_id: InputField = { + label: 'Insert ID', + type: 'string', + description: + 'Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time.' +} + +export const library: InputField = { + label: 'Library', + type: 'string', + description: 'The name of the library that generated the event.', + default: { + '@path': '$.context.library.name' + } +} + + +export const userAgent: InputField ={ + label: 'User Agent', + type: 'string', + description: 'The user agent of the device sending the event.', + default: { + '@path': '$.context.userAgent' + } +} + +export const userAgentParsing: InputField = { + label: 'User Agent Parsing', + type: 'boolean', + description: + 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', + default: true +} + +export const includeRawUserAgent: InputField ={ + label: 'Include Raw User Agent', + type: 'boolean', + description: + 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', + default: false +} + + +export const utm_properties: InputField = { + label: 'UTM Properties', + type: 'object', + description: 'UTM Tracking Properties', + properties: { + utm_source: { + label: 'UTM Source', + type: 'string' + }, + utm_medium: { + label: 'UTM Medium', + type: 'string' + }, + utm_campaign: { + label: 'UTM Campaign', + type: 'string' + }, + utm_term: { + label: 'UTM Term', + type: 'string' + }, + utm_content: { + label: 'UTM Content', + type: 'string' + } + }, + default: { + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' } + } +} + +export const referrer: InputField = { + label: 'Referrer', + type: 'string', + description: + 'The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer”', + default: { + '@path': '$.context.page.referrer' + } +} + +/** + * The common fields defined by Amplitude's events api + * @see {@link https://developers.amplitude.com/docs/http-api-v2#keys-for-the-event-argument} + */ +export const eventSchema: Record = { + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, + userAgent +} + +export const userAgentData: InputField = { + label: 'User Agent Data', + type: 'object', + description: 'The user agent data of device sending the event', + properties: { + model: { + label: 'Model', + type: 'string' + }, + platformVersion: { + label: 'PlatformVersion', + type: 'string' + } + }, + default: { + model: { '@path': '$.context.userAgentData.model' }, + platformVersion: { '@path': '$.context.userAgentData.platformVersion' } + } +} + +export const min_id_length: InputField = { + label: 'Minimum ID Length', + description: 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', + allowNull: true, + type: 'integer' +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts index 4e6bea16693..c5199a79cc4 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -2,7 +2,8 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import dayjs from '../../../lib/dayjs' -import { getEndpointByRegion } from '../regional-endpoints' +import { getEndpointByRegion } from '../common-functions' +import { user_id, device_id, insert_id, time, min_id_length } from '../fields' const action: ActionDefinition = { title: 'Group Identify User', @@ -11,43 +12,12 @@ const action: ActionDefinition = { defaultSubscription: 'type = "group"', fields: { user_id: { - label: 'User ID', - type: 'string', - allowNull: true, - description: - 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. If either user ID or device ID is present, an associate user to group call will be made.', - default: { - '@path': '$.userId' - } - }, - device_id: { - label: 'Device ID', - type: 'string', - description: - 'A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. If either user ID or device ID is present, an associate user to group call will be made.', - default: { - '@if': { - exists: { '@path': '$.context.device.id' }, - then: { '@path': '$.context.device.id' }, - else: { '@path': '$.anonymousId' } - } - } - }, - insert_id: { - label: 'Insert ID', - type: 'string', - description: - 'Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time.' - }, - time: { - label: 'Timestamp', - type: 'string', - description: - 'The timestamp of the event. If time is not sent with the event, it will be set to the request upload time.', - default: { - '@path': '$.timestamp' - } + ...user_id, + description: 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. If either user ID or device ID is present, an associate user to group call will be made.' }, + device_id, + insert_id, + time, group_properties: { label: 'Group Properties', type: 'object', @@ -68,13 +38,7 @@ const action: ActionDefinition = { description: 'The value of the group', required: true }, - min_id_length: { - label: 'Minimum ID Length', - description: - 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' - } + min_id_length }, perform: async (request, { payload, settings }) => { const groupAssociation = { [payload.group_type]: payload.group_value } diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/constants.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/constants.ts new file mode 100644 index 00000000000..c9aecd192f2 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/constants.ts @@ -0,0 +1 @@ +export const KEYS_TO_OMIT = ['utm_properties', 'referrer','autocaptureAttributionEnabled','autocaptureAttributionSet','autocaptureAttributionSetOnce','autocaptureAttributionUnset'] \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/functions.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/functions.ts new file mode 100644 index 00000000000..2c59589697d --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/functions.ts @@ -0,0 +1,47 @@ +import { Payload as IdentifyUserPayload} from './generated-types' +import { RequestClient, omit } from '@segment/actions-core' +import { Settings } from '../generated-types' +import { getEndpointByRegion, getUserProperties, parseUserAgentProperties } from '../common-functions' +import { AmplitudeProfileJSON } from './types' +import { KEYS_TO_OMIT } from './constants' + +export function send(request: RequestClient, payload: IdentifyUserPayload, settings: Settings) { + const { + userAgent, + userAgentParsing, + includeRawUserAgent, + userAgentData, + min_id_length, + platform, + library, + user_id, + ...rest + } = omit(payload, KEYS_TO_OMIT) + + const user_properties = getUserProperties(payload) + + const event: AmplitudeProfileJSON = { + ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), + ...(includeRawUserAgent && { user_agent: userAgent }), + ...rest, + ...{ user_id: user_id || null }, + ...(platform ? { platform: platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } : {}), + ...(library === 'analytics.js' && !platform ? { platform: 'Web' } : {}), + ...(user_properties ? { user_properties } : {}), + library: 'segment' + } + + const url = getEndpointByRegion('identify', settings.endpoint) + + const body = new URLSearchParams() + body.set('api_key', settings.apiKey) + body.set('identification', JSON.stringify(event)) + if (typeof min_id_length === 'number' && min_id_length > 0) { + body.set('options', JSON.stringify({ min_id_length }) ) + } + + return request(url, { + method: 'post', + body + }) +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts index d74f1000048..3b5ae433108 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts @@ -1,6 +1,28 @@ // Generated file. DO NOT MODIFY IT BY HAND. export interface Payload { + /** + * Utility field used to detect if Autocapture Attribution Plugin is enabled. + */ + autocaptureAttributionEnabled?: boolean + /** + * Utility field used to detect if any attribution values need to be set. + */ + autocaptureAttributionSet?: { + [k: string]: unknown + } + /** + * Utility field used to detect if any attribution values need to be set_once. + */ + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } + /** + * Utility field used to detect if any attribution values need to be unset. + */ + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** * A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present. */ diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts index ca38dca62e4..bd9070e3299 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts @@ -1,13 +1,36 @@ -import { ActionDefinition, omit, removeUndefined } from '@segment/actions-core' +import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { convertUTMProperties } from '../utm' -import { convertReferrerProperty } from '../referrer' -import { parseUserAgentProperties } from '../user-agent' -import { mergeUserProperties } from '../merge-user-properties' -import { AmplitudeEvent } from '../logEvent' -import { getEndpointByRegion } from '../regional-endpoints' -import { userAgentData } from '../properties' +import { send } from './functions' +import { autocaptureFields } from '../autocapture-fields' +import { + user_id, + device_id, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + insert_id, + userAgent, + userAgentParsing, + includeRawUserAgent, + utm_properties, + referrer, + min_id_length, + library, + userAgentData +} from '../fields' const action: ActionDefinition = { title: 'Identify User', @@ -15,145 +38,36 @@ const action: ActionDefinition = { 'Set the user ID for a particular device ID or update user properties without sending an event to Amplitude.', defaultSubscription: 'type = "identify"', fields: { + ...autocaptureFields, user_id: { - label: 'User ID', - type: 'string', - allowNull: true, - description: - 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present.', - default: { - '@path': '$.userId' - } + ...user_id, + description: 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present.' }, device_id: { - label: 'Device ID', - type: 'string', - description: - 'A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present.', - default: { - '@if': { - exists: { '@path': '$.context.device.id' }, - then: { '@path': '$.context.device.id' }, - else: { '@path': '$.anonymousId' } - } - } + ...device_id, + description: 'A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present.' }, user_properties: { - label: 'User Properties', - type: 'object', - description: - 'Additional data tied to the user in Amplitude. Each distinct value will show up as a user segment on the Amplitude dashboard. Object depth may not exceed 40 layers. **Note:** You can store property values in an array and date values are transformed into string values.', - default: { - '@path': '$.traits' - } + ...user_properties, + description: 'Additional data tied to the user in Amplitude. Each distinct value will show up as a user segment on the Amplitude dashboard. Object depth may not exceed 40 layers. **Note:** You can store property values in an array and date values are transformed into string values.' }, groups: { - label: 'Groups', - type: 'object', - description: - "Groups of users for Amplitude's account-level reporting feature. Note: You can only track up to 5 groups. Any groups past that threshold will not be tracked. **Note:** This feature is only available to Amplitude Enterprise customers who have purchased the Amplitude Accounts add-on." - }, - app_version: { - label: 'App Version', - type: 'string', - description: 'Version of the app the user is on.', - default: { - '@path': '$.context.app.version' - } - }, - platform: { - label: 'Platform', - type: 'string', - description: "The platform of the user's device.", - default: { - '@path': '$.context.device.type' - } - }, - os_name: { - label: 'OS Name', - type: 'string', - description: "The mobile operating system or browser of the user's device.", - default: { - '@path': '$.context.os.name' - } - }, - os_version: { - label: 'OS Version', - type: 'string', - description: "The version of the mobile operating system or browser of the user's device.", - default: { - '@path': '$.context.os.version' - } - }, - device_brand: { - label: 'Device Brand', - type: 'string', - description: "The brand of user's the device.", - default: { - '@path': '$.context.device.brand' - } - }, - device_manufacturer: { - label: 'Device Manufacturer', - type: 'string', - description: "The manufacturer of the user's device.", - default: { - '@path': '$.context.device.manufacturer' - } - }, - device_model: { - label: 'Device Model', - type: 'string', - description: "The model of the user's device.", - default: { - '@path': '$.context.device.model' - } - }, - carrier: { - label: 'Carrier', - type: 'string', - description: "The user's mobile carrier.", - default: { - '@path': '$.context.network.carrier' - } - }, - country: { - label: 'Country', - type: 'string', - description: 'The country in which the user is located.', - default: { - '@path': '$.context.location.country' - } - }, - region: { - label: 'Region', - type: 'string', - description: 'The geographical region in which the user is located.', - default: { - '@path': '$.context.location.region' - } - }, - city: { - label: 'City', - type: 'string', - description: 'The city in which the user is located.', - default: { - '@path': '$.context.location.city' - } - }, - dma: { - label: 'Designated Market Area', - type: 'string', - description: 'The Designated Market Area in which the user is located.' - }, - language: { - label: 'Language', - type: 'string', - description: 'Language the user has set on their device or browser.', - default: { - '@path': '$.context.locale' - } - }, + ...groups, + description: "Groups of users for Amplitude's account-level reporting feature. Note: You can only track up to 5 groups. Any groups past that threshold will not be tracked. **Note:** This feature is only available to Amplitude Enterprise customers who have purchased the Amplitude Accounts add-on." + }, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, paying: { label: 'Is Paying', type: 'boolean', @@ -164,149 +78,19 @@ const action: ActionDefinition = { type: 'string', description: 'The version of the app the user was first on.' }, - insert_id: { - label: 'Insert ID', - type: 'string', - description: - 'Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time.' - }, - userAgent: { - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } - }, - userAgentParsing: { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', - default: true - }, - includeRawUserAgent: { - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false - }, - utm_properties: { - label: 'UTM Properties', - type: 'object', - description: 'UTM Tracking Properties', - properties: { - utm_source: { - label: 'UTM Source', - type: 'string' - }, - utm_medium: { - label: 'UTM Medium', - type: 'string' - }, - utm_campaign: { - label: 'UTM Campaign', - type: 'string' - }, - utm_term: { - label: 'UTM Term', - type: 'string' - }, - utm_content: { - label: 'UTM Content', - type: 'string' - } - }, - default: { - utm_source: { '@path': '$.context.campaign.source' }, - utm_medium: { '@path': '$.context.campaign.medium' }, - utm_campaign: { '@path': '$.context.campaign.name' }, - utm_term: { '@path': '$.context.campaign.term' }, - utm_content: { '@path': '$.context.campaign.content' } - } - }, - referrer: { - label: 'Referrer', - type: 'string', - description: - 'The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer”', - default: { - '@path': '$.context.page.referrer' - } - }, - min_id_length: { - label: 'Minimum ID Length', - description: - 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' - }, - library: { - label: 'Library', - type: 'string', - description: 'The name of the library that generated the event.', - default: { - '@path': '$.context.library.name' - } - }, + insert_id, + userAgent, + userAgentParsing, + includeRawUserAgent, + utm_properties, + referrer, + min_id_length, + library, userAgentData }, - perform: (request, { payload, settings }) => { - const { - utm_properties, - referrer, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - min_id_length, - library, - ...rest - } = payload - - let options - const properties = rest as AmplitudeEvent - - if (properties.platform) { - properties.platform = properties.platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') - } - - if (library === 'analytics.js' && !properties.platform) { - properties.platform = 'Web' - } - - if (Object.keys(utm_properties ?? {}).length || referrer) { - properties.user_properties = mergeUserProperties( - omit(properties.user_properties ?? {}, ['utm_properties', 'referrer']), - convertUTMProperties(payload), - convertReferrerProperty(payload) - ) - } - - if (min_id_length && min_id_length > 0) { - options = JSON.stringify({ min_id_length }) - } - - const identification = JSON.stringify({ - // Conditionally parse user agent using amplitude's library - ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), - ...(includeRawUserAgent && { user_agent: userAgent }), - // Make sure any top-level properties take precedence over user-agent properties - ...removeUndefined(properties), - library: 'segment' - }) - - return request(getEndpointByRegion('identify', settings.endpoint), { - method: 'post', - body: new URLSearchParams({ - api_key: settings.apiKey, - identification, - options - } as Record) - }) + return send(request, payload, settings) } } -export default action +export default action \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/types.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/types.ts new file mode 100644 index 00000000000..56dc5263ade --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/types.ts @@ -0,0 +1,23 @@ +import { UserProperties, ParsedUA, UserAgentData } from '../types' + +export interface AmplitudeProfileJSON { + user_id?: string | null + device_id?: string + user_properties?: UserProperties + groups?: Record + app_version?: string + platform?: string + device_brand?: string + carrier?: string + country?: string + region?: string + city?: string + dma?: string + language?: string + paying?: boolean + start_version?: string + insert_id?: string + user_agent?: ParsedUA | string + userAgentData?: UserAgentData + library?: string +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/index.ts b/packages/destination-actions/src/destinations/amplitude/index.ts index 3dc313a3015..b2490cd661e 100644 --- a/packages/destination-actions/src/destinations/amplitude/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/index.ts @@ -6,7 +6,7 @@ import mapUser from './mapUser' import groupIdentifyUser from './groupIdentifyUser' import logPurchase from './logPurchase' import type { Settings } from './generated-types' -import { getEndpointByRegion } from './regional-endpoints' +import { getEndpointByRegion } from './common-functions' import logEventV2 from './logEventV2' diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts index dd60578bfa2..403e31a854e 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts @@ -178,21 +178,27 @@ export interface Payload { [k: string]: unknown }[] /** - * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - use_batch_endpoint?: boolean + autocaptureAttributionEnabled?: boolean /** - * The user agent of the device sending the event. + * Utility field used to detect if any attribution values need to be set. */ - userAgent?: string + autocaptureAttributionSet?: { + [k: string]: unknown + } /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + * Utility field used to detect if any attribution values need to be set_once. */ - userAgentParsing?: boolean + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * Utility field used to detect if any attribution values need to be unset. */ - includeRawUserAgent?: boolean + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** * UTM Tracking Properties */ @@ -207,6 +213,22 @@ export interface Payload { * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” */ referrer?: string + /** + * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). + */ + use_batch_endpoint?: boolean + /** + * The user agent of the device sending the event. + */ + userAgent?: string + /** + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + */ + userAgentParsing?: boolean + /** + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + */ + includeRawUserAgent?: boolean /** * Amplitude has a default minimum id lenght of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index 7632cfb04b2..9a3e9ed388f 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -3,8 +3,8 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { userAgentData } from '../properties' -import { autocaptureFields } from '../logEventV2/autocapture-fields' -import { send } from '../logEventV2/autocapture-attribution' +import { autocaptureFields } from '../autocapture-fields' +import { send } from '../events-functions' const action: ActionDefinition = { title: 'Log Event', diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts deleted file mode 100644 index 8044e830b84..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/autocapture-attribution.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { AMPLITUDE_ATTRIBUTION_KEYS } from '@segment/actions-shared' -import { Payload as LogEventPayload} from '../logEvent/generated-types' -import { Payload as LogEventV2Payload} from './generated-types' -import { Payload as PurchasePayload } from '../logPurchase/generated-types' -import { UserProperties, EventRevenue, AmplitudeEvent, JSON } from './types' -import { RequestClient, omit, removeUndefined } from '@segment/actions-core' -import { Settings } from '../generated-types' -import { KEYS_TO_OMIT } from './constants' -import { parseUserAgentProperties } from '../user-agent' -import { formatSessionId } from '../convert-timestamp' -import { getEndpointByRegion } from '../regional-endpoints' -import dayjs from '../../../lib/dayjs' - -export function send(request: RequestClient, payload: LogEventPayload | LogEventV2Payload | PurchasePayload, settings: Settings, isPurchaseEvent: boolean) { - const { - time, - session_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - min_id_length, - platform, - library, - user_id, - products = [], - ...rest - } = omit(payload, KEYS_TO_OMIT) - - let trackRevenuePerProduct = false - if ('trackRevenuePerProduct' in payload) { - trackRevenuePerProduct = payload.trackRevenuePerProduct || false - } - - const user_properties = getUserProperties(payload) - - const events: AmplitudeEvent[] = [{ - ...(userAgentParsing && parseUserAgentProperties(userAgent, userAgentData)), - ...(includeRawUserAgent && { user_agent: userAgent }), - ...rest, - ...{ user_id: user_id || null }, - ...(platform ? { platform: platform.replace(/ios/i, 'iOS').replace(/android/i, 'Android') } : {}), - ...(library === 'analytics.js' && !platform ? { platform: 'Web' } : {}), - ...(time && dayjs.utc(time).isValid() ? { time: dayjs.utc(time).valueOf() } : {}), - ...(session_id && dayjs.utc(session_id).isValid() ? { session_id: formatSessionId(session_id) } : {}), - ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}), - ...(user_properties ? { user_properties } : {}), - ...(products.length && trackRevenuePerProduct ? {} : getRevenueProperties(payload)), - library: 'segment' - }] - - if(isPurchaseEvent){ - const mainEvent = events[0] - for (const product of products) { - events.push({ - ...mainEvent, - ...(trackRevenuePerProduct ? getRevenueProperties(product as EventRevenue) : {}), - event_properties: product, - event_type: 'Product Purchased', - insert_id: mainEvent.insert_id ? `${mainEvent.insert_id}-${events.length + 1}` : undefined - }) - } - } - - const json: JSON = { - api_key: settings.apiKey, - events: events.map(removeUndefined), - ...(typeof min_id_length === 'number' && min_id_length > 0 ? { options: { min_id_length } } : {}) - } - - const endpoint = getEndpointByRegion(payload.use_batch_endpoint ? 'batch' : 'httpapi', settings.endpoint) - - return request(endpoint, { - method: 'post', - json - }) -} - -function getUserProperties(payload: LogEventPayload | LogEventV2Payload | PurchasePayload): UserProperties { - const { - autocaptureAttributionEnabled, - autocaptureAttributionSet, - autocaptureAttributionSetOnce, - autocaptureAttributionUnset, - user_properties - } = payload - - let setOnce: UserProperties['$setOnce'] = {} - let setAlways: UserProperties['$set'] = {} - let add: UserProperties['$add'] = {} - - if ('utm_properties' in payload || 'referrer' in payload) { - // For LogPurchase and LogEvent Actions - const { utm_properties, referrer } = payload - setAlways = { - ...(referrer ? { referrer } : {}), - ...(utm_properties || {}) - } - setOnce = { - ...(referrer ? { initial_referrer: referrer } : {}), - ...(utm_properties - ? Object.fromEntries(Object.entries(utm_properties).map(([k, v]) => [`initial_${k}`, v])) - : {}) - } - } - else if ('setOnce' in payload || 'setAlways' in payload || 'add' in payload){ - // For LogEventV2 Action - setOnce = payload.setOnce as UserProperties['$setOnce'] - setAlways = payload.setAlways as UserProperties['$set'] - add = payload.add as UserProperties['$add'] - } - - if (autocaptureAttributionEnabled) { - // If autocapture attribution is enabled, we need to make sure that attribution keys are not sent from the setAlways and setOnce fields - for (const key of AMPLITUDE_ATTRIBUTION_KEYS) { - if( typeof setAlways === "object" && setAlways !== null){ - delete setAlways[key] - } - if(typeof setOnce === "object" && setOnce !== null){ - delete setOnce[`initial_${key}`] - } - } - } - - const userProperties = { - ...user_properties, - ...(compact(autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } as { [k: string]: string } : setOnce as { [k: string]: string }) - ? { $setOnce: autocaptureAttributionEnabled ? { ...setOnce, ...autocaptureAttributionSetOnce } as { [k: string]: string }: setOnce as { [k: string]: string }} - : {}), - ...(compact(autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }) - ? { $set: autocaptureAttributionEnabled ? { ...setAlways, ...autocaptureAttributionSet } as { [k: string]: string }: setAlways as { [k: string]: string }} - : {}), - ...(compact(add) ? { $add: add as { [k: string]: string } } : {}), - ...(compact(autocaptureAttributionEnabled ? autocaptureAttributionUnset : {}) - ? { $unset: autocaptureAttributionEnabled ? autocaptureAttributionUnset as { [k: string]: string } : {} as { [k: string]: string } } - : {}) - } - return userProperties -} - -function getRevenueProperties(payload: EventRevenue): EventRevenue { - let { revenue } = payload - const { quantity, price, revenueType, productId } = payload - if (typeof quantity === 'number' && typeof price === 'number') { - revenue = quantity * price - } - if (!revenue) { - return {} - } - return { - revenue, - revenueType: revenueType ?? 'Purchase', - quantity: typeof quantity === 'number' ? Math.round(quantity) : undefined, - price: price, - productId - } -} - -function compact(object: { [k: string]: unknown } | undefined): boolean { - return Object.keys(Object.fromEntries(Object.entries(object ?? {}).filter(([_, v]) => v !== ''))).length > 0 -} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index f17c300de01..edae1bcc0fc 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -3,8 +3,8 @@ import { eventSchema } from '../event-schema' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { userAgentData } from '../properties' -import { autocaptureFields } from './autocapture-fields' -import { send } from './autocapture-attribution' +import { autocaptureFields } from '../autocapture-fields' +import { send } from '../events-functions' const action: ActionDefinition = { title: 'Log Event V2', diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index 4eb55fd34a5..416b5ebadce 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -3,8 +3,8 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { userAgentData } from '../properties' -import { autocaptureFields } from '../logEventV2/autocapture-fields' -import { send } from '../logEventV2/autocapture-attribution' +import { autocaptureFields } from '../autocapture-fields' +import { send } from '../events-functions' const action: ActionDefinition = { title: 'Log Purchase', diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index 88f4cc3ea0e..648355b3d56 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -1,5 +1,5 @@ import type { ActionDefinition } from '@segment/actions-core' -import { getEndpointByRegion } from '../regional-endpoints' +import { getEndpointByRegion } from '../common-functions' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' diff --git a/packages/destination-actions/src/destinations/amplitude/merge-user-properties.ts b/packages/destination-actions/src/destinations/amplitude/merge-user-properties.ts deleted file mode 100644 index 57cb66acbd7..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/merge-user-properties.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface AmplitudeUserProperties { - $set?: object - $setOnce?: object - [k: string]: unknown -} - -export function mergeUserProperties(...properties: AmplitudeUserProperties[]): AmplitudeUserProperties { - return properties.reduce((prev, current) => { - const hasSet = prev.$set || current.$set - const hasSetOnce = prev.$setOnce || current.$setOnce - return { - ...prev, - ...current, - ...(hasSet && { $set: { ...prev.$set, ...current.$set } }), - ...(hasSetOnce && { $setOnce: { ...prev.$setOnce, ...current.$setOnce } }) - } - }, {}) -} diff --git a/packages/destination-actions/src/destinations/amplitude/properties.ts b/packages/destination-actions/src/destinations/amplitude/properties.ts deleted file mode 100644 index b012a7088ca..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/properties.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { InputField } from '@segment/actions-core/destination-kit/types' - -export const userAgentData: InputField = { - label: 'User Agent Data', - type: 'object', - description: 'The user agent data of device sending the event', - properties: { - model: { - label: 'Model', - type: 'string' - }, - platformVersion: { - label: 'PlatformVersion', - type: 'string' - } - }, - default: { - model: { '@path': '$.context.userAgentData.model' }, - platformVersion: { '@path': '$.context.userAgentData.platformVersion' } - } -} diff --git a/packages/destination-actions/src/destinations/amplitude/referrer.ts b/packages/destination-actions/src/destinations/amplitude/referrer.ts deleted file mode 100644 index 575f850713c..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/referrer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Payload as IdentifyPayload } from './identifyUser/generated-types' -import { Payload as LogPayload } from './logEvent/generated-types' -import { AmplitudeUserProperties } from './merge-user-properties' -type Payload = IdentifyPayload | LogPayload - -/** - * takes a payload object and converts it to a valid user_properties object for use in amplitude events - * - * @param payload an identify or log payload - * @returns a valid user_properties object suitable for injection into an AmplitudeEvent - */ -export function convertReferrerProperty(payload: Payload): AmplitudeUserProperties { - const { referrer } = payload - - if (!referrer) return {} - - return { - $set: { referrer }, - $setOnce: { initial_referrer: referrer } - } -} diff --git a/packages/destination-actions/src/destinations/amplitude/regional-endpoints.ts b/packages/destination-actions/src/destinations/amplitude/regional-endpoints.ts deleted file mode 100644 index bf49f1c86ed..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/regional-endpoints.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const endpoints = { - batch: { - north_america: 'https://api2.amplitude.com/batch', - europe: 'https://api.eu.amplitude.com/batch' - }, - deletions: { - north_america: 'https://amplitude.com/api/2/deletions/users', - europe: 'https://analytics.eu.amplitude.com/api/2/deletions/users' - }, - httpapi: { - north_america: 'https://api2.amplitude.com/2/httpapi', - europe: 'https://api.eu.amplitude.com/2/httpapi' - }, - identify: { - north_america: 'https://api2.amplitude.com/identify', - europe: 'https://api.eu.amplitude.com/identify' - }, - groupidentify: { - north_america: 'https://api2.amplitude.com/groupidentify', - europe: 'https://api.eu.amplitude.com/groupidentify' - }, - usermap: { - north_america: 'https://api.amplitude.com/usermap', - europe: 'https://api.eu.amplitude.com/usermap' - }, - usersearch: { - north_america: 'https://amplitude.com/api/2/usersearch', - europe: 'https://analytics.eu.amplitude.com/api/2/usersearch' - } -} - -type Region = 'north_america' | 'europe' - -/** - * Retrieves Amplitude API endpoints for a given region. If the region - * provided does not exist, the region defaults to 'north_america'. - * - * @param endpoint name of the API endpoint - * @param region data residency region - * @returns regional API endpoint - */ -export function getEndpointByRegion(endpoint: keyof typeof endpoints, region?: string): string { - return endpoints[endpoint][region as Region] ?? endpoints[endpoint]['north_america'] -} - -export default endpoints diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts b/packages/destination-actions/src/destinations/amplitude/types.ts similarity index 77% rename from packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts rename to packages/destination-actions/src/destinations/amplitude/types.ts index d23c3db28d5..547ad6522c3 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/types.ts +++ b/packages/destination-actions/src/destinations/amplitude/types.ts @@ -1,6 +1,6 @@ -import { ParsedUA } from '../user-agent' +export type Region = 'north_america' | 'europe' -export interface AmplitudeEvent extends EventRevenue { +export interface AmplitudeEventJSON extends EventRevenue { user_id?: string | null device_id?: string event_type: string @@ -11,11 +11,7 @@ export interface AmplitudeEvent extends EventRevenue { groups?: Record app_version?: string platform?: string - os_name?: string - os_version?: string device_brand?: string - device_manufacturer?: string - device_model?: string carrier?: string country?: string region?: string @@ -34,11 +30,20 @@ export interface AmplitudeEvent extends EventRevenue { library?: string use_batch_endpoint?: boolean user_agent?: ParsedUA | string - includeRawUserAgent?: boolean - userAgentData?: { - model?: string - platformVersion?: string - } + userAgentData?: UserAgentData +} + +export interface ParsedUA { + os_name?: string + os_version?: string + device_model?: string + device_type?: string + device_manufacturer?: string +} + +export interface UserAgentData { + model?: string + platformVersion?: string } export interface EventRevenue { @@ -57,10 +62,10 @@ export interface UserProperties { [k: string]: unknown } -export interface JSON { +export interface JSON_PAYLOAD { api_key: string - events: AmplitudeEvent[] + events: AmplitudeEventJSON[] options?: { min_id_length: number } -} +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/user-agent.ts b/packages/destination-actions/src/destinations/amplitude/user-agent.ts deleted file mode 100644 index 95f82847469..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/user-agent.ts +++ /dev/null @@ -1,37 +0,0 @@ -import UaParser from '@amplitude/ua-parser-js' - -export interface ParsedUA { - os_name?: string - os_version?: string - device_model?: string - device_type?: string - device_manufacturer?: string -} - -interface UserAgentData { - model?: string - platformVersion?: string -} - -export function parseUserAgentProperties(userAgent?: string, userAgentData?: UserAgentData): ParsedUA { - if (!userAgent) { - return {} - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const parser = new UaParser(userAgent) - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const device = parser.getDevice() - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const os = parser.getOS() - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const browser = parser.getBrowser() - - return { - os_name: os.name ?? browser.name, - os_version: userAgentData?.platformVersion ?? browser.major, - device_manufacturer: device.vendor, - device_model: userAgentData?.model ?? device.model ?? os.name, - device_type: device.type - } -} diff --git a/packages/destination-actions/src/destinations/amplitude/utm.ts b/packages/destination-actions/src/destinations/amplitude/utm.ts deleted file mode 100644 index 49c54e6fa53..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/utm.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AmplitudeUserProperties } from './merge-user-properties' - -interface Payload { - utm_properties?: UTMProperties - user_properties?: AmplitudeUserProperties -} - -interface UTMProperties { - utm_source?: string - utm_medium?: string - utm_campaign?: string - utm_term?: string - utm_content?: string -} - -interface InitialUTMProperties { - initial_utm_source?: string - initial_utm_medium?: string - initial_utm_campaign?: string - initial_utm_term?: string - initial_utm_content?: string -} - -/** - * Take a compatible payload that contains a `utm_properties` key and converts it to a user_properties object suitable for injection into an amplitude event - * - * @param payload an event payload that contains a utm_properties property - * @returns a user_properties object suitable for amplitude events - */ -export function convertUTMProperties(payload: Payload): AmplitudeUserProperties { - const { utm_properties } = payload - - if (!utm_properties) return {} - - const set: UTMProperties = {} - const setOnce: InitialUTMProperties = {} - - Object.entries(utm_properties).forEach(([key, value]) => { - set[key as keyof UTMProperties] = value - setOnce[`initial_${key}` as keyof InitialUTMProperties] = value - }) - - return { - $set: set, - $setOnce: setOnce - } -} From 09a502fc514f59e77da77949221f23fdc081c06e Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 06:43:26 +0000 Subject: [PATCH 31/38] refactoring ... --- .../src/destinations/amplitude/fields.ts | 121 +++++++-- .../destinations/amplitude/logEvent/index.ts | 236 +++++++----------- .../amplitude/logEventV2/index.ts | 106 +------- .../amplitude/logPurchase/index.ts | 176 ++----------- .../destinations/amplitude/mapUser/index.ts | 21 +- 5 files changed, 220 insertions(+), 440 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/fields.ts b/packages/destination-actions/src/destinations/amplitude/fields.ts index 4871dc104c4..c9dcdaf2ebf 100644 --- a/packages/destination-actions/src/destinations/amplitude/fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/fields.ts @@ -368,34 +368,34 @@ export const utm_properties: InputField = { type: 'object', description: 'UTM Tracking Properties', properties: { - utm_source: { - label: 'UTM Source', - type: 'string' - }, - utm_medium: { - label: 'UTM Medium', - type: 'string' - }, - utm_campaign: { - label: 'UTM Campaign', - type: 'string' - }, - utm_term: { - label: 'UTM Term', - type: 'string' - }, - utm_content: { - label: 'UTM Content', - type: 'string' - } + utm_source: { + label: 'UTM Source', + type: 'string' + }, + utm_medium: { + label: 'UTM Medium', + type: 'string' + }, + utm_campaign: { + label: 'UTM Campaign', + type: 'string' + }, + utm_term: { + label: 'UTM Term', + type: 'string' + }, + utm_content: { + label: 'UTM Content', + type: 'string' + } }, default: { - utm_source: { '@path': '$.context.campaign.source' }, - utm_medium: { '@path': '$.context.campaign.medium' }, - utm_campaign: { '@path': '$.context.campaign.name' }, - utm_term: { '@path': '$.context.campaign.term' }, - utm_content: { '@path': '$.context.campaign.content' } - } + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' } + } } export const referrer: InputField = { @@ -477,4 +477,73 @@ export const min_id_length: InputField = { description: 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', allowNull: true, type: 'integer' +} + +export const use_batch_endpoint: InputField ={ + label: 'Use Batch Endpoint', + description: + "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", + type: 'boolean', + default: false +} + +export const products: InputField = { + label: 'Products', + description: 'The list of products purchased.', + type: 'object', + multiple: true, + additionalProperties: true, + properties: { + price: { + label: 'Price', + type: 'number', + description: + 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.' + }, + quantity: { + label: 'Quantity', + type: 'integer', + description: 'The quantity of the item purchased. Defaults to 1 if not specified.' + }, + revenue: { + label: 'Revenue', + type: 'number', + description: + 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds.' + }, + productId: { + label: 'Product ID', + type: 'string', + description: + 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.' + }, + revenueType: { + label: 'Revenue Type', + type: 'string', + description: + 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.' + } + }, + default: { + '@arrayPath': [ + '$.properties.products', + { + price: { + '@path': 'price' + }, + revenue: { + '@path': 'revenue' + }, + quantity: { + '@path': 'quantity' + }, + productId: { + '@path': 'productId' + }, + revenueType: { + '@path': 'revenueType' + } + } + ] + } } \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index 9a3e9ed388f..e00576c51ba 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -1,157 +1,107 @@ -import { eventSchema } from '../event-schema' import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { userAgentData } from '../properties' import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' +import { + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, + products, + utm_properties, + referrer, + use_batch_endpoint, + userAgent, + userAgentParsing, + includeRawUserAgent, + userAgentData, + min_id_length +} from '../fields' + const action: ActionDefinition = { title: 'Log Event', description: 'Send an event to Amplitude.', defaultSubscription: 'type = "track"', fields: { - ...eventSchema, - products: { - label: 'Products', - description: 'The list of products purchased.', - type: 'object', - multiple: true, - additionalProperties: true, - properties: { - price: { - label: 'Price', - type: 'number', - description: - 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.' - }, - quantity: { - label: 'Quantity', - type: 'integer', - description: 'The quantity of the item purchased. Defaults to 1 if not specified.' - }, - revenue: { - label: 'Revenue', - type: 'number', - description: - 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds.' - }, - productId: { - label: 'Product ID', - type: 'string', - description: - 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.' - }, - revenueType: { - label: 'Revenue Type', - type: 'string', - description: - 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.' - } - }, - default: { - '@arrayPath': [ - '$.properties.products', - { - price: { - '@path': 'price' - }, - revenue: { - '@path': 'revenue' - }, - quantity: { - '@path': 'quantity' - }, - productId: { - '@path': 'productId' - }, - revenueType: { - '@path': 'revenueType' - } - } - ] - } - }, + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, + products, ...autocaptureFields, - utm_properties: { - label: 'UTM Properties', - type: 'object', - description: 'UTM Tracking Properties', - properties: { - utm_source: { - label: 'UTM Source', - type: 'string' - }, - utm_medium: { - label: 'UTM Medium', - type: 'string' - }, - utm_campaign: { - label: 'UTM Campaign', - type: 'string' - }, - utm_term: { - label: 'UTM Term', - type: 'string' - }, - utm_content: { - label: 'UTM Content', - type: 'string' - } - }, - default: { - utm_source: { '@path': '$.context.campaign.source' }, - utm_medium: { '@path': '$.context.campaign.medium' }, - utm_campaign: { '@path': '$.context.campaign.name' }, - utm_term: { '@path': '$.context.campaign.term' }, - utm_content: { '@path': '$.context.campaign.content' } - } - }, - referrer: { - label: 'Referrer', - type: 'string', - description: - 'The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer”', - default: { - '@path': '$.context.page.referrer' - } - }, - use_batch_endpoint: { - label: 'Use Batch Endpoint', - description: - "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", - type: 'boolean', - default: false - }, - userAgent: { - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } - }, - userAgentParsing: { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', - default: true - }, - includeRawUserAgent: { - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false - }, - min_id_length: { - label: 'Minimum ID Length', - description: - 'Amplitude has a default minimum id lenght of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' - }, + utm_properties, + referrer, + use_batch_endpoint, + userAgent, + userAgentParsing, + includeRawUserAgent, + min_id_length, userAgentData }, perform: (request, { payload, settings }) => { @@ -159,4 +109,4 @@ const action: ActionDefinition = { } } -export default action +export default action \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index edae1bcc0fc..e40169b27fa 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,77 +1,16 @@ import { ActionDefinition} from '@segment/actions-core' -import { eventSchema } from '../event-schema' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { userAgentData } from '../properties' import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' +import { products, use_batch_endpoint, userAgent, userAgentParsing, includeRawUserAgent, min_id_length, userAgentData } from '../fields' const action: ActionDefinition = { title: 'Log Event V2', description: 'Send an event to Amplitude', defaultSubscription: 'type = "track"', fields: { - ...eventSchema, - products: { - label: 'Products', - description: 'The list of products purchased.', - type: 'object', - multiple: true, - additionalProperties: true, - properties: { - price: { - label: 'Price', - type: 'number', - description: - 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.' - }, - quantity: { - label: 'Quantity', - type: 'integer', - description: 'The quantity of the item purchased. Defaults to 1 if not specified.' - }, - revenue: { - label: 'Revenue', - type: 'number', - description: - 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds.' - }, - productId: { - label: 'Product ID', - type: 'string', - description: - 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.' - }, - revenueType: { - label: 'Revenue Type', - type: 'string', - description: - 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.' - } - }, - default: { - '@arrayPath': [ - '$.properties.products', - { - price: { - '@path': 'price' - }, - revenue: { - '@path': 'revenue' - }, - quantity: { - '@path': 'quantity' - }, - productId: { - '@path': 'productId' - }, - revenueType: { - '@path': 'revenueType' - } - } - ] - } - }, + products, ...autocaptureFields, setOnce: { label: 'Set Once', @@ -162,42 +101,11 @@ const action: ActionDefinition = { additionalProperties: true, defaultObjectUI: 'keyvalue' }, - use_batch_endpoint: { - label: 'Use Batch Endpoint', - description: - "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", - type: 'boolean', - default: false - }, - userAgent: { - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } - }, - userAgentParsing: { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field.', - default: true - }, - includeRawUserAgent: { - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false - }, - min_id_length: { - label: 'Minimum ID Length', - description: - 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' - }, + use_batch_endpoint, + userAgent, + userAgentParsing, + includeRawUserAgent, + min_id_length, userAgentData }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index 416b5ebadce..dc602b08fe2 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -1,10 +1,19 @@ -import { eventSchema } from '../event-schema' import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { userAgentData } from '../properties' import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' +import { + products, + utm_properties, + referrer, + use_batch_endpoint, + userAgent, + userAgentParsing, + includeRawUserAgent, + min_id_length, + userAgentData +} from '../fields' const action: ActionDefinition = { title: 'Log Purchase', @@ -18,162 +27,15 @@ const action: ActionDefinition = { type: 'boolean', default: false }, - ...eventSchema, - products: { - label: 'Products', - description: 'The list of products purchased.', - type: 'object', - multiple: true, - additionalProperties: true, - properties: { - price: { - label: 'Price', - type: 'number', - description: - 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.' - }, - quantity: { - label: 'Quantity', - type: 'integer', - description: 'The quantity of the item purchased. Defaults to 1 if not specified.' - }, - revenue: { - label: 'Revenue', - type: 'number', - description: - 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds.', - depends_on: { - match: 'any', - conditions: [ - { - fieldKey: 'price', - operator: 'is', - value: '' - }, - { - fieldKey: 'quantity', - operator: 'is', - value: '' - } - ] - } - }, - productId: { - label: 'Product ID', - type: 'string', - description: - 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.' - }, - revenueType: { - label: 'Revenue Type', - type: 'string', - description: - 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.' - } - }, - default: { - '@arrayPath': [ - '$.properties.products', - { - price: { - '@path': 'price' - }, - revenue: { - '@path': 'revenue' - }, - quantity: { - '@path': 'quantity' - }, - productId: { - '@path': 'productId' - }, - revenueType: { - '@path': 'revenueType' - } - } - ] - } - }, + products, ...autocaptureFields, - utm_properties: { - label: 'UTM Properties', - type: 'object', - description: 'UTM Tracking Properties', - properties: { - utm_source: { - label: 'UTM Source', - type: 'string' - }, - utm_medium: { - label: 'UTM Medium', - type: 'string' - }, - utm_campaign: { - label: 'UTM Campaign', - type: 'string' - }, - utm_term: { - label: 'UTM Term', - type: 'string' - }, - utm_content: { - label: 'UTM Content', - type: 'string' - } - }, - default: { - utm_source: { '@path': '$.context.campaign.source' }, - utm_medium: { '@path': '$.context.campaign.medium' }, - utm_campaign: { '@path': '$.context.campaign.name' }, - utm_term: { '@path': '$.context.campaign.term' }, - utm_content: { '@path': '$.context.campaign.content' } - } - }, - referrer: { - label: 'Referrer', - type: 'string', - description: - 'The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer”', - default: { - '@path': '$.context.page.referrer' - } - }, - use_batch_endpoint: { - label: 'Use Batch Endpoint', - description: - "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", - type: 'boolean', - default: false - }, - userAgent: { - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } - }, - userAgentParsing: { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', - default: true - }, - includeRawUserAgent: { - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false - }, - min_id_length: { - label: 'Minimum ID Length', - description: - 'Amplitude has a default minimum id lenght of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' - }, + utm_properties, + referrer, + use_batch_endpoint, + userAgent, + userAgentParsing, + includeRawUserAgent, + min_id_length, userAgentData }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index 648355b3d56..b2cb984c4ed 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -2,20 +2,17 @@ import type { ActionDefinition } from '@segment/actions-core' import { getEndpointByRegion } from '../common-functions' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' +import { + user_id, + min_id_length +} from '../fields' const action: ActionDefinition = { title: 'Map User', description: 'Merge two users together that would otherwise have different User IDs tracked in Amplitude.', defaultSubscription: 'type = "alias"', fields: { - user_id: { - label: 'User ID', - type: 'string', - description: 'The User ID to be associated.', - default: { - '@path': '$.previousId' - } - }, + user_id, global_user_id: { label: 'Global User ID', type: 'string', @@ -24,13 +21,7 @@ const action: ActionDefinition = { '@path': '$.userId' } }, - min_id_length: { - label: 'Minimum ID Length', - description: - 'Amplitude has a default minimum id length (`min_id_length`) of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' - } + min_id_length }, perform: (request, { payload, settings }) => { const { min_id_length } = payload From 420acd7e04ffeee762dee63a33347d1af6e227c1 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 09:26:39 +0000 Subject: [PATCH 32/38] refactoring --- .../amplitude/groupIdentifyUser/fields.ts | 22 +++ .../amplitude/groupIdentifyUser/index.ts | 36 ++-- .../amplitude/identifyUser/fields.ts | 12 ++ .../amplitude/identifyUser/index.ts | 16 +- .../destinations/amplitude/logEvent/index.ts | 72 +++---- .../amplitude/logEventV2/fields.ts | 101 ++++++++++ .../amplitude/logEventV2/index.ts | 178 +++++++++--------- .../amplitude/logPurchase/fields.ts | 9 + .../amplitude/logPurchase/index.ts | 85 ++++++++- .../destinations/amplitude/mapUser/fields.ts | 10 + .../destinations/amplitude/mapUser/index.ts | 14 +- 11 files changed, 382 insertions(+), 173 deletions(-) create mode 100644 packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/identifyUser/fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/logEventV2/fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/logPurchase/fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/mapUser/fields.ts diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/fields.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/fields.ts new file mode 100644 index 00000000000..d0469b3fbe5 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/fields.ts @@ -0,0 +1,22 @@ +import { InputField } from '@segment/actions-core' + +export const group_properties: InputField = { + label: 'Group Properties', + type: 'object', + description: 'Additional data tied to the group in Amplitude.', + default: { '@path': '$.traits'} +} + +export const group_type: InputField = { + label: 'Group Type', + type: 'string', + description: 'The type of the group', + required: true +} + +export const group_value: InputField = { + label: 'Group Value', + type: 'string', + description: 'The value of the group', + required: true +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts index c5199a79cc4..1cef1df0d0f 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -3,7 +3,18 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import dayjs from '../../../lib/dayjs' import { getEndpointByRegion } from '../common-functions' -import { user_id, device_id, insert_id, time, min_id_length } from '../fields' +import { + user_id, + device_id, + insert_id, + time, + min_id_length +} from '../fields' +import { + group_properties, + group_type, + group_value +} from './fields' const action: ActionDefinition = { title: 'Group Identify User', @@ -18,26 +29,9 @@ const action: ActionDefinition = { device_id, insert_id, time, - group_properties: { - label: 'Group Properties', - type: 'object', - description: 'Additional data tied to the group in Amplitude.', - default: { - '@path': '$.traits' - } - }, - group_type: { - label: 'Group Type', - type: 'string', - description: 'The type of the group', - required: true - }, - group_value: { - label: 'Group Value', - type: 'string', - description: 'The value of the group', - required: true - }, + group_properties, + group_type, + group_value, min_id_length }, perform: async (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/fields.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/fields.ts new file mode 100644 index 00000000000..443be5f437f --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/fields.ts @@ -0,0 +1,12 @@ +import { InputField } from '@segment/actions-core' + +export const paying: InputField = { + label: 'Is Paying', + type: 'boolean', + description: 'Whether the user is paying or not.' +} +export const start_version: InputField = { + label: 'Initial Version', + type: 'string', + description: 'The version of the app the user was first on.' +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts index bd9070e3299..fce227fb29f 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts @@ -31,6 +31,10 @@ import { library, userAgentData } from '../fields' +import { + paying, + start_version +} from './fields' const action: ActionDefinition = { title: 'Identify User', @@ -68,16 +72,8 @@ const action: ActionDefinition = { city, dma, language, - paying: { - label: 'Is Paying', - type: 'boolean', - description: 'Whether the user is paying or not.' - }, - start_version: { - label: 'Initial Version', - type: 'string', - description: 'The version of the app the user was first on.' - }, + paying, + start_version, insert_id, userAgent, userAgentParsing, diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index e00576c51ba..c03b85339da 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -5,42 +5,42 @@ import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' import { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, products, utm_properties, referrer, diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/fields.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/fields.ts new file mode 100644 index 00000000000..fffb479d99c --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/fields.ts @@ -0,0 +1,101 @@ +import { InputField } from '@segment/actions-core' + +export const trackRevenuePerProduct: InputField = { + label: 'Track Revenue Per Product', + description: + 'When enabled, track revenue with each product within the event. When disabled, track total revenue once for the event.', + type: 'boolean', + default: false +} + +export const setOnce: InputField = { + label: 'Set Once', + description: "The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored.", + type: 'object', + additionalProperties: true, + properties: { + initial_referrer: { + label: 'Initial Referrer', + type: 'string', + description: 'The referrer of the web request.' + }, + initial_utm_source: { + label: 'Initial UTM Source', + type: 'string' + }, + initial_utm_medium: { + label: 'Initial UTM Medium', + type: 'string' + }, + initial_utm_campaign: { + label: 'Initial UTM Campaign', + type: 'string' + }, + initial_utm_term: { + label: 'Initial UTM Term', + type: 'string' + }, + initial_utm_content: { + label: 'Initial UTM Content', + type: 'string' + } + }, + default: { + initial_referrer: { '@path': '$.context.page.referrer' }, + initial_utm_source: { '@path': '$.context.campaign.source' }, + initial_utm_medium: { '@path': '$.context.campaign.medium' }, + initial_utm_campaign: { '@path': '$.context.campaign.name' }, + initial_utm_term: { '@path': '$.context.campaign.term' }, + initial_utm_content: { '@path': '$.context.campaign.content' } + } +} + +export const setAlways: InputField = { + label: 'Set Always', + description: "The following fields will be set as user properties for every event. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored.", + type: 'object', + additionalProperties: true, + properties: { + referrer: { + label: 'Referrer', + type: 'string' + }, + utm_source: { + label: 'UTM Source', + type: 'string' + }, + utm_medium: { + label: 'UTM Medium', + type: 'string' + }, + utm_campaign: { + label: 'UTM Campaign', + type: 'string' + }, + utm_term: { + label: 'UTM Term', + type: 'string' + }, + utm_content: { + label: 'UTM Content', + type: 'string' + } + }, + default: { + referrer: { '@path': '$.context.page.referrer' }, + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' } + } +} + +export const add: InputField = { + label: 'Add', + description: + "Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0.", + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue' +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index e40169b27fa..395391620e0 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -3,104 +3,102 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' -import { products, use_batch_endpoint, userAgent, userAgentParsing, includeRawUserAgent, min_id_length, userAgentData } from '../fields' +import { + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, + products, + use_batch_endpoint, + userAgent, + userAgentParsing, + includeRawUserAgent, + min_id_length, + userAgentData +} from '../fields' +import { + setOnce, + setAlways, + add, +} from './fields' + const action: ActionDefinition = { title: 'Log Event V2', description: 'Send an event to Amplitude', defaultSubscription: 'type = "track"', fields: { + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, products, + setOnce, + setAlways, + add, ...autocaptureFields, - setOnce: { - label: 'Set Once', - description: "The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored.", - type: 'object', - additionalProperties: true, - properties: { - initial_referrer: { - label: 'Initial Referrer', - type: 'string', - description: 'The referrer of the web request.' - }, - initial_utm_source: { - label: 'Initial UTM Source', - type: 'string' - }, - initial_utm_medium: { - label: 'Initial UTM Medium', - type: 'string' - }, - initial_utm_campaign: { - label: 'Initial UTM Campaign', - type: 'string' - }, - initial_utm_term: { - label: 'Initial UTM Term', - type: 'string' - }, - initial_utm_content: { - label: 'Initial UTM Content', - type: 'string' - } - }, - default: { - initial_referrer: { '@path': '$.context.page.referrer' }, - initial_utm_source: { '@path': '$.context.campaign.source' }, - initial_utm_medium: { '@path': '$.context.campaign.medium' }, - initial_utm_campaign: { '@path': '$.context.campaign.name' }, - initial_utm_term: { '@path': '$.context.campaign.term' }, - initial_utm_content: { '@path': '$.context.campaign.content' } - } - }, - setAlways: { - label: 'Set Always', - description: "The following fields will be set as user properties for every event. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored.", - type: 'object', - additionalProperties: true, - properties: { - referrer: { - label: 'Referrer', - type: 'string' - }, - utm_source: { - label: 'UTM Source', - type: 'string' - }, - utm_medium: { - label: 'UTM Medium', - type: 'string' - }, - utm_campaign: { - label: 'UTM Campaign', - type: 'string' - }, - utm_term: { - label: 'UTM Term', - type: 'string' - }, - utm_content: { - label: 'UTM Content', - type: 'string' - } - }, - default: { - referrer: { '@path': '$.context.page.referrer' }, - utm_source: { '@path': '$.context.campaign.source' }, - utm_medium: { '@path': '$.context.campaign.medium' }, - utm_campaign: { '@path': '$.context.campaign.name' }, - utm_term: { '@path': '$.context.campaign.term' }, - utm_content: { '@path': '$.context.campaign.content' } - } - }, - add: { - label: 'Add', - description: - "Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0.", - type: 'object', - additionalProperties: true, - defaultObjectUI: 'keyvalue' - }, use_batch_endpoint, userAgent, userAgentParsing, diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/fields.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/fields.ts new file mode 100644 index 00000000000..23790e8e463 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/fields.ts @@ -0,0 +1,9 @@ +import { InputField } from '@segment/actions-core' + +export const trackRevenuePerProduct: InputField = { + label: 'Track Revenue Per Product', + description: + 'When enabled, track revenue with each product within the event. When disabled, track total revenue once for the event.', + type: 'boolean', + default: false +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index dc602b08fe2..a3411424f42 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -4,6 +4,42 @@ import type { Payload } from './generated-types' import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' import { + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, products, utm_properties, referrer, @@ -14,19 +50,54 @@ import { min_id_length, userAgentData } from '../fields' +import { + trackRevenuePerProduct +} +from './fields' + const action: ActionDefinition = { title: 'Log Purchase', description: 'Send an event to Amplitude.', defaultSubscription: 'type = "track"', fields: { - trackRevenuePerProduct: { - label: 'Track Revenue Per Product', - description: - 'When enabled, track revenue with each product within the event. When disabled, track total revenue once for the event.', - type: 'boolean', - default: false - }, + trackRevenuePerProduct, + user_id, + device_id, + event_type, + session_id, + time, + event_properties, + user_properties, + groups, + app_version, + platform, + os_name, + os_version, + device_brand, + device_manufacturer, + device_model, + carrier, + country, + region, + city, + dma, + language, + price, + quantity, + revenue, + productId, + revenueType, + location_lat, + location_lng, + ip, + idfa, + idfv, + adid, + android_id, + event_id, + insert_id, + library, products, ...autocaptureFields, utm_properties, diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/fields.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/fields.ts new file mode 100644 index 00000000000..d69bac24bb3 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/fields.ts @@ -0,0 +1,10 @@ +import { InputField } from '@segment/actions-core' + +export const global_user_id: InputField = { + label: 'Global User ID', + type: 'string', + description: 'The Global User ID to associate with the User ID.', + default: { + '@path': '$.userId' + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index b2cb984c4ed..067ce8edca3 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -6,6 +6,9 @@ import { user_id, min_id_length } from '../fields' +import { + global_user_id +} from './fields' const action: ActionDefinition = { title: 'Map User', @@ -13,14 +16,7 @@ const action: ActionDefinition = { defaultSubscription: 'type = "alias"', fields: { user_id, - global_user_id: { - label: 'Global User ID', - type: 'string', - description: 'The Global User ID to associate with the User ID.', - default: { - '@path': '$.userId' - } - }, + global_user_id, min_id_length }, perform: (request, { payload, settings }) => { @@ -37,4 +33,4 @@ const action: ActionDefinition = { } } -export default action +export default action \ No newline at end of file From 3ee421ab6b2cf0d0feea5fa392fdb2e50e79fd2f Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 09:29:04 +0000 Subject: [PATCH 33/38] types --- .../groupIdentifyUser/generated-types.ts | 4 +- .../amplitude/identifyUser/generated-types.ts | 26 +++++----- .../amplitude/logEvent/generated-types.ts | 4 +- .../amplitude/logEventV2/generated-types.ts | 52 +++++++++---------- .../amplitude/logPurchase/generated-types.ts | 4 +- .../amplitude/mapUser/generated-types.ts | 6 +-- 6 files changed, 46 insertions(+), 50 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts index 05340838d5a..637c234de56 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts @@ -6,7 +6,7 @@ export interface Payload { */ user_id?: string | null /** - * A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. If either user ID or device ID is present, an associate user to group call will be made. + * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. */ device_id?: string /** @@ -16,7 +16,7 @@ export interface Payload { /** * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ - time?: string + time?: string | number /** * Additional data tied to the group in Amplitude. */ diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts index 3b5ae433108..d746e71424f 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts @@ -44,55 +44,55 @@ export interface Payload { [k: string]: unknown } /** - * Version of the app the user is on. + * The current version of your application. */ app_version?: string /** - * The platform of the user's device. + * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. */ platform?: string /** - * The mobile operating system or browser of the user's device. + * The name of the mobile operating system or browser that the user is using. */ os_name?: string /** - * The version of the mobile operating system or browser of the user's device. + * The version of the mobile operating system or browser the user is using. */ os_version?: string /** - * The brand of user's the device. + * The device brand that the user is using. */ device_brand?: string /** - * The manufacturer of the user's device. + * The device manufacturer that the user is using. */ device_manufacturer?: string /** - * The model of the user's device. + * The device model that the user is using. */ device_model?: string /** - * The user's mobile carrier. + * The carrier that the user is using. */ carrier?: string /** - * The country in which the user is located. + * The current country of the user. */ country?: string /** - * The geographical region in which the user is located. + * The current region of the user. */ region?: string /** - * The city in which the user is located. + * The current city of the user. */ city?: string /** - * The Designated Market Area in which the user is located. + * The current Designated Market Area of the user. */ dma?: string /** - * Language the user has set on their device or browser. + * The language set by the user. */ language?: string /** diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts index 403e31a854e..3c90665ca5b 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts @@ -120,7 +120,7 @@ export interface Payload { */ location_lng?: number /** - * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. You can submit a request to Amplitude's platform specialist team here to configure this for you. + * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. */ ip?: string /** @@ -230,7 +230,7 @@ export interface Payload { */ includeRawUserAgent?: boolean /** - * Amplitude has a default minimum id lenght of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. + * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null /** diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 44d344d64b2..e0e691067d2 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -75,10 +75,6 @@ export interface Payload { * The current country of the user. */ country?: string - /** - * The current region of the user. - */ - region?: string /** * The current city of the user. */ @@ -120,7 +116,7 @@ export interface Payload { */ location_lng?: number /** - * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. You can submit a request to Amplitude's platform specialist team here to configure this for you. + * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. */ ip?: string /** @@ -177,28 +173,6 @@ export interface Payload { revenueType?: string [k: string]: unknown }[] - /** - * Utility field used to detect if Autocapture Attribution Plugin is enabled. - */ - autocaptureAttributionEnabled?: boolean - /** - * Utility field used to detect if any attribution values need to be set. - */ - autocaptureAttributionSet?: { - [k: string]: unknown - } - /** - * Utility field used to detect if any attribution values need to be set_once. - */ - autocaptureAttributionSetOnce?: { - [k: string]: unknown - } - /** - * Utility field used to detect if any attribution values need to be unset. - */ - autocaptureAttributionUnset?: { - [k: string]: unknown - } /** * The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. */ @@ -232,6 +206,28 @@ export interface Payload { add?: { [k: string]: unknown } + /** + * Utility field used to detect if Autocapture Attribution Plugin is enabled. + */ + autocaptureAttributionEnabled?: boolean + /** + * Utility field used to detect if any attribution values need to be set. + */ + autocaptureAttributionSet?: { + [k: string]: unknown + } + /** + * Utility field used to detect if any attribution values need to be set_once. + */ + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } + /** + * Utility field used to detect if any attribution values need to be unset. + */ + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ @@ -241,7 +237,7 @@ export interface Payload { */ userAgent?: string /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field. + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field */ userAgentParsing?: boolean /** diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts index 4ee181286f7..85d52626920 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts @@ -124,7 +124,7 @@ export interface Payload { */ location_lng?: number /** - * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. You can submit a request to Amplitude's platform specialist team here to configure this for you. + * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. */ ip?: string /** @@ -234,7 +234,7 @@ export interface Payload { */ includeRawUserAgent?: boolean /** - * Amplitude has a default minimum id lenght of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. + * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null /** diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/generated-types.ts index d0940523ef4..7cbff6ac760 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/generated-types.ts @@ -2,15 +2,15 @@ export interface Payload { /** - * The User ID to be associated. + * A readable ID specified by you. Must have a minimum length of 5 characters. Required unless device ID is present. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. */ - user_id?: string + user_id?: string | null /** * The Global User ID to associate with the User ID. */ global_user_id?: string /** - * Amplitude has a default minimum id length (`min_id_length`) of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. + * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null } From b275d64a580a3b429ef5e7aede2652e204e30e1a Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 10:35:51 +0000 Subject: [PATCH 34/38] refactoring --- .../amplitude/autocapture-fields.ts | 2 +- .../src/destinations/amplitude/fields.ts | 549 ------------------ .../amplitude/groupIdentifyUser/index.ts | 23 +- .../amplitude/identifyUser/index.ts | 75 +-- .../destinations/amplitude/logEvent/index.ts | 105 +--- .../amplitude/logEventV2/index.ts | 106 +--- .../amplitude/logPurchase/index.ts | 110 +--- .../destinations/amplitude/mapUser/index.ts | 10 +- 8 files changed, 57 insertions(+), 923 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/amplitude/fields.ts diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts index 3d3f6c2dec3..a401d656f8d 100644 --- a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts +++ b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts @@ -1,6 +1,6 @@ import type { InputField } from '@segment/actions-core' import { DESTINATION_INTEGRATION_NAME } from './events-constants' -export const autocaptureFields: Record = { +export const autocapture_fields: Record = { autocaptureAttributionEnabled: { label: 'Autocapture Attribution Enabled', description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', diff --git a/packages/destination-actions/src/destinations/amplitude/fields.ts b/packages/destination-actions/src/destinations/amplitude/fields.ts deleted file mode 100644 index c9dcdaf2ebf..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/fields.ts +++ /dev/null @@ -1,549 +0,0 @@ -import { InputField } from '@segment/actions-core' - -export const user_id: InputField = { - label: 'User ID', - type: 'string', - allowNull: true, - description: 'A readable ID specified by you. Must have a minimum length of 5 characters. Required unless device ID is present. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event.', - default: { - '@path': '$.userId' - } -} - -export const device_id: InputField = { - label: 'Device ID', - type: 'string', - description: 'A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID.', - default: { - '@if': { - exists: { '@path': '$.context.device.id' }, - then: { '@path': '$.context.device.id' }, - else: { '@path': '$.anonymousId' } - } - } -} - -export const event_type: InputField = { - label: 'Event Type', - type: 'string', - description: 'A unique identifier for your event.', - required: true, - default: { - '@path': '$.event' - } -} - -export const session_id: InputField = { - label: 'Session ID', - type: 'datetime', - description: 'The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source.', - default: { - '@path': '$.integrations.Actions Amplitude.session_id' - } -} - -export const time: InputField = { - label: 'Timestamp', - type: 'datetime', - description: 'The timestamp of the event. If time is not sent with the event, it will be set to the request upload time.', - default: { - '@path': '$.timestamp' - } -} - -export const event_properties: InputField = { - label: 'Event Properties', - type: 'object', - description: - 'An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', - default: { - '@path': '$.properties' - } -} - -export const user_properties: InputField = { - label: 'User Properties', - type: 'object', - description: - 'An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', - default: { - '@path': '$.traits' - } -} - -export const groups: InputField = { - label: 'Groups', - type: 'object', - description: - 'Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on.' -} - -export const app_version: InputField = { - label: 'App Version', - type: 'string', - description: 'The current version of your application.', - default: { - '@path': '$.context.app.version' - } -} - - -export const platform: InputField = { - label: 'Platform', - type: 'string', - description: - 'Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent.', - default: { - '@path': '$.context.device.type' - } -} - -export const os_name: InputField = { - label: 'OS Name', - type: 'string', - description: 'The name of the mobile operating system or browser that the user is using.', - default: { - '@path': '$.context.os.name' - } -} - - -export const os_version: InputField = { - label: 'OS Version', - type: 'string', - description: 'The version of the mobile operating system or browser the user is using.', - default: { - '@path': '$.context.os.version' - } -}; - -export const device_brand: InputField = { - label: 'Device Brand', - type: 'string', - description: 'The device brand that the user is using.', - default: { - '@path': '$.context.device.brand' - } -}; - -export const device_manufacturer: InputField = { - label: 'Device Manufacturer', - type: 'string', - description: 'The device manufacturer that the user is using.', - default: { - '@path': '$.context.device.manufacturer' - } -}; - -export const device_model: InputField = { - label: 'Device Model', - type: 'string', - description: 'The device model that the user is using.', - default: { - '@path': '$.context.device.model' - } -}; - -export const carrier: InputField = { - label: 'Carrier', - type: 'string', - description: 'The carrier that the user is using.', - default: { - '@path': '$.context.network.carrier' - } -}; - -export const country: InputField = { - label: 'Country', - type: 'string', - description: 'The current country of the user.', - default: { - '@path': '$.context.location.country' - } -} - -export const region: InputField = { - label: 'Region', - type: 'string', - description: 'The current region of the user.', - default: { - '@path': '$.context.location.region' - } -} - -export const city: InputField = { - label: 'City', - type: 'string', - description: 'The current city of the user.', - default: { - '@path': '$.context.location.city' - } -} - -export const dma: InputField = { - label: 'Designated Market Area', - type: 'string', - description: 'The current Designated Market Area of the user.' -} - -export const language: InputField = { - label: 'Language', - type: 'string', - description: 'The language set by the user.', - default: { - '@path': '$.context.locale' - } -} - -export const price: InputField = { - label: 'Price', - type: 'number', - description: - 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.', - default: { - '@path': '$.properties.price' - } -} - -export const quantity: InputField = { - label: 'Quantity', - type: 'integer', - description: 'The quantity of the item purchased. Defaults to 1 if not specified.', - default: { - '@path': '$.properties.quantity' - } -} - -export const revenue: InputField = { - label: 'Revenue', - type: 'number', - description: - 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode.', - default: { - '@path': '$.properties.revenue' - } -} - -export const productId: InputField = { - label: 'Product ID', - type: 'string', - description: - 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.', - default: { - '@path': '$.properties.productId' - } -} - -export const revenueType: InputField = { - label: 'Revenue Type', - type: 'string', - description: - 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.', - default: { - '@path': '$.properties.revenueType' - } -} - -export const location_lat: InputField = { - label: 'Latitude', - type: 'number', - description: 'The current Latitude of the user.', - default: { - '@path': '$.context.location.latitude' - } -} - -export const location_lng: InputField = { - label: 'Longtitude', - type: 'number', - description: 'The current Longitude of the user.', - default: { - '@path': '$.context.location.longitude' - } -} - -export const ip: InputField = { - label: 'IP Address', - type: 'string', - description: - 'The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user\'s location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers.', - default: { - '@path': '$.context.ip' - } -} - -export const idfa: InputField = { - label: 'Identifier For Advertiser (IDFA)', - type: 'string', - description: 'Identifier for Advertiser. _(iOS)_', - default: { - '@if': { - exists: { '@path': '$.context.device.advertisingId' }, - then: { '@path': '$.context.device.advertisingId' }, - else: { '@path': '$.context.device.idfa' } - } - } -} - -export const idfv: InputField = { - label: 'Identifier For Vendor (IDFV)', - type: 'string', - description: 'Identifier for Vendor. _(iOS)_', - default: { - '@path': '$.context.device.id' - } -} - -export const adid: InputField = { - label: 'Google Play Services Advertising ID', - type: 'string', - description: 'Google Play Services advertising ID. _(Android)_', - default: { - '@if': { - exists: { '@path': '$.context.device.advertisingId' }, - then: { '@path': '$.context.device.advertisingId' }, - else: { '@path': '$.context.device.idfa' } - } - } -} - -export const android_id: InputField = { - label: 'Android ID', - type: 'string', - description: 'Android ID (not the advertising ID). _(Android)_' -} - -export const event_id: InputField = { - label: 'Event ID', - type: 'integer', - description: - 'An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously.' -} - -export const insert_id: InputField = { - label: 'Insert ID', - type: 'string', - description: - 'Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time.' -} - -export const library: InputField = { - label: 'Library', - type: 'string', - description: 'The name of the library that generated the event.', - default: { - '@path': '$.context.library.name' - } -} - - -export const userAgent: InputField ={ - label: 'User Agent', - type: 'string', - description: 'The user agent of the device sending the event.', - default: { - '@path': '$.context.userAgent' - } -} - -export const userAgentParsing: InputField = { - label: 'User Agent Parsing', - type: 'boolean', - description: - 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', - default: true -} - -export const includeRawUserAgent: InputField ={ - label: 'Include Raw User Agent', - type: 'boolean', - description: - 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', - default: false -} - - -export const utm_properties: InputField = { - label: 'UTM Properties', - type: 'object', - description: 'UTM Tracking Properties', - properties: { - utm_source: { - label: 'UTM Source', - type: 'string' - }, - utm_medium: { - label: 'UTM Medium', - type: 'string' - }, - utm_campaign: { - label: 'UTM Campaign', - type: 'string' - }, - utm_term: { - label: 'UTM Term', - type: 'string' - }, - utm_content: { - label: 'UTM Content', - type: 'string' - } - }, - default: { - utm_source: { '@path': '$.context.campaign.source' }, - utm_medium: { '@path': '$.context.campaign.medium' }, - utm_campaign: { '@path': '$.context.campaign.name' }, - utm_term: { '@path': '$.context.campaign.term' }, - utm_content: { '@path': '$.context.campaign.content' } - } -} - -export const referrer: InputField = { - label: 'Referrer', - type: 'string', - description: - 'The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer”', - default: { - '@path': '$.context.page.referrer' - } -} - -/** - * The common fields defined by Amplitude's events api - * @see {@link https://developers.amplitude.com/docs/http-api-v2#keys-for-the-event-argument} - */ -export const eventSchema: Record = { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - userAgent -} - -export const userAgentData: InputField = { - label: 'User Agent Data', - type: 'object', - description: 'The user agent data of device sending the event', - properties: { - model: { - label: 'Model', - type: 'string' - }, - platformVersion: { - label: 'PlatformVersion', - type: 'string' - } - }, - default: { - model: { '@path': '$.context.userAgentData.model' }, - platformVersion: { '@path': '$.context.userAgentData.platformVersion' } - } -} - -export const min_id_length: InputField = { - label: 'Minimum ID Length', - description: 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', - allowNull: true, - type: 'integer' -} - -export const use_batch_endpoint: InputField ={ - label: 'Use Batch Endpoint', - description: - "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", - type: 'boolean', - default: false -} - -export const products: InputField = { - label: 'Products', - description: 'The list of products purchased.', - type: 'object', - multiple: true, - additionalProperties: true, - properties: { - price: { - label: 'Price', - type: 'number', - description: - 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.' - }, - quantity: { - label: 'Quantity', - type: 'integer', - description: 'The quantity of the item purchased. Defaults to 1 if not specified.' - }, - revenue: { - label: 'Revenue', - type: 'number', - description: - 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds.' - }, - productId: { - label: 'Product ID', - type: 'string', - description: - 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.' - }, - revenueType: { - label: 'Revenue Type', - type: 'string', - description: - 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.' - } - }, - default: { - '@arrayPath': [ - '$.properties.products', - { - price: { - '@path': 'price' - }, - revenue: { - '@path': 'revenue' - }, - quantity: { - '@path': 'quantity' - }, - productId: { - '@path': 'productId' - }, - revenueType: { - '@path': 'revenueType' - } - } - ] - } -} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts index 1cef1df0d0f..d7e575a1eac 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -3,18 +3,9 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import dayjs from '../../../lib/dayjs' import { getEndpointByRegion } from '../common-functions' -import { - user_id, - device_id, - insert_id, - time, - min_id_length -} from '../fields' -import { - group_properties, - group_type, - group_value -} from './fields' +import { common_fields } from '../common-fields' +import { group_properties, group_type, group_value } from './fields' +import { time } from '../misc-fields' const action: ActionDefinition = { title: 'Group Identify User', @@ -22,17 +13,15 @@ const action: ActionDefinition = { 'Set or update properties of particular groups. Note that these updates will only affect events going forward.', defaultSubscription: 'type = "group"', fields: { + ...common_fields, user_id: { - ...user_id, + ...common_fields.user_id, description: 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. If either user ID or device ID is present, an associate user to group call will be made.' }, - device_id, - insert_id, time, group_properties, group_type, - group_value, - min_id_length + group_value }, perform: async (request, { payload, settings }) => { const groupAssociation = { [payload.group_type]: payload.group_value } diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts index fce227fb29f..74859617113 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts @@ -2,39 +2,11 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { send } from './functions' -import { autocaptureFields } from '../autocapture-fields' -import { - user_id, - device_id, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - insert_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - utm_properties, - referrer, - min_id_length, - library, - userAgentData -} from '../fields' -import { - paying, - start_version -} from './fields' +import { autocapture_fields } from '../autocapture-fields' +import { common_fields } from '../common-fields' +import { common_track_identify_fields} from '../common-track-identify-fields' +import { paying, start_version } from './fields' +import { min_id_length } from '../misc-fields' const action: ActionDefinition = { title: 'Identify User', @@ -42,47 +14,28 @@ const action: ActionDefinition = { 'Set the user ID for a particular device ID or update user properties without sending an event to Amplitude.', defaultSubscription: 'type = "identify"', fields: { - ...autocaptureFields, + ...common_fields, + ...common_track_identify_fields, + ...autocapture_fields, user_id: { - ...user_id, + ...common_fields.user_id, description: 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present.' }, device_id: { - ...device_id, + ...common_track_identify_fields.device_id, description: 'A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present.' }, user_properties: { - ...user_properties, + ...common_track_identify_fields.user_properties, description: 'Additional data tied to the user in Amplitude. Each distinct value will show up as a user segment on the Amplitude dashboard. Object depth may not exceed 40 layers. **Note:** You can store property values in an array and date values are transformed into string values.' }, groups: { - ...groups, + ...common_track_identify_fields.groups, description: "Groups of users for Amplitude's account-level reporting feature. Note: You can only track up to 5 groups. Any groups past that threshold will not be tracked. **Note:** This feature is only available to Amplitude Enterprise customers who have purchased the Amplitude Accounts add-on." }, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - paying, - start_version, - insert_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - utm_properties, - referrer, min_id_length, - library, - userAgentData + paying, + start_version }, perform: (request, { payload, settings }) => { return send(request, payload, settings) diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index c03b85339da..a642d80e2ef 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -1,108 +1,23 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' - -import { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - products, - utm_properties, - referrer, - use_batch_endpoint, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - min_id_length -} from '../fields' +import { common_fields } from '../common-fields' +import { common_track_fields } from '../common-track-fields' +import { common_track_identify_fields } from '../common-track-identify-fields' +import { autocapture_fields } from '../autocapture-fields' +import { min_id_length } from '../misc-fields' const action: ActionDefinition = { title: 'Log Event', description: 'Send an event to Amplitude.', defaultSubscription: 'type = "track"', fields: { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - products, - ...autocaptureFields, - utm_properties, - referrer, - use_batch_endpoint, - userAgent, - userAgentParsing, - includeRawUserAgent, - min_id_length, - userAgentData + ...common_fields, + ...common_track_fields, + ...common_track_identify_fields, + ...autocapture_fields, + min_id_length }, perform: (request, { payload, settings }) => { return send(request, payload, settings, false) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 395391620e0..695cd42867e 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,110 +1,28 @@ import { ActionDefinition} from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { autocaptureFields } from '../autocapture-fields' +import { autocapture_fields } from '../autocapture-fields' import { send } from '../events-functions' -import { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - products, - use_batch_endpoint, - userAgent, - userAgentParsing, - includeRawUserAgent, - min_id_length, - userAgentData -} from '../fields' -import { - setOnce, - setAlways, - add, -} from './fields' - +import { common_fields } from '../common-fields' +import { common_track_fields } from '../common-track-fields' +import { common_track_identify_fields } from '../common-track-identify-fields' +import { trackRevenuePerProduct, setOnce, setAlways, add } from './fields' +import { min_id_length } from '../misc-fields' const action: ActionDefinition = { title: 'Log Event V2', description: 'Send an event to Amplitude', defaultSubscription: 'type = "track"', fields: { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - products, + ...common_fields, + trackRevenuePerProduct, setOnce, setAlways, add, - ...autocaptureFields, - use_batch_endpoint, - userAgent, - userAgentParsing, - includeRawUserAgent, - min_id_length, - userAgentData + ...common_track_fields, + ...common_track_identify_fields, + ...autocapture_fields, + min_id_length }, perform: (request, { payload, settings }) => { return send(request, payload, settings, false) diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index a3411424f42..bcedd48cd29 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -1,60 +1,13 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { autocaptureFields } from '../autocapture-fields' import { send } from '../events-functions' -import { - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - products, - utm_properties, - referrer, - use_batch_endpoint, - userAgent, - userAgentParsing, - includeRawUserAgent, - min_id_length, - userAgentData -} from '../fields' -import { - trackRevenuePerProduct -} -from './fields' - +import { common_fields } from '../common-fields' +import { common_track_fields } from '../common-track-fields' +import { common_track_identify_fields } from '../common-track-identify-fields' +import { autocapture_fields } from '../autocapture-fields' +import { trackRevenuePerProduct } from './fields' +import { min_id_length } from '../misc-fields' const action: ActionDefinition = { title: 'Log Purchase', @@ -62,52 +15,11 @@ const action: ActionDefinition = { defaultSubscription: 'type = "track"', fields: { trackRevenuePerProduct, - user_id, - device_id, - event_type, - session_id, - time, - event_properties, - user_properties, - groups, - app_version, - platform, - os_name, - os_version, - device_brand, - device_manufacturer, - device_model, - carrier, - country, - region, - city, - dma, - language, - price, - quantity, - revenue, - productId, - revenueType, - location_lat, - location_lng, - ip, - idfa, - idfv, - adid, - android_id, - event_id, - insert_id, - library, - products, - ...autocaptureFields, - utm_properties, - referrer, - use_batch_endpoint, - userAgent, - userAgentParsing, - includeRawUserAgent, - min_id_length, - userAgentData + ...common_fields, + ...common_track_fields, + ...common_track_identify_fields, + ...autocapture_fields, + min_id_length }, perform: (request, { payload, settings }) => { return send(request, payload, settings, true) diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index 067ce8edca3..4ce1cfec7f7 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -2,13 +2,9 @@ import type { ActionDefinition } from '@segment/actions-core' import { getEndpointByRegion } from '../common-functions' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { - user_id, - min_id_length -} from '../fields' -import { - global_user_id -} from './fields' +import { user_id } from '../common-fields' +import { global_user_id } from './fields' +import { min_id_length } from '../misc-fields' const action: ActionDefinition = { title: 'Map User', From 9286928f21f41243cfe7322372bf5bc54b8dcd24 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 10:50:31 +0000 Subject: [PATCH 35/38] refactoring --- .../amplitude/autocapture-fields.ts | 32 ----- .../amplitude/groupIdentifyUser/index.ts | 4 +- .../amplitude/identifyUser/index.ts | 8 +- .../src/destinations/amplitude/index.ts | 112 +++++++++--------- .../destinations/amplitude/logEvent/index.ts | 10 +- .../amplitude/logEventV2/index.ts | 10 +- .../amplitude/logPurchase/index.ts | 10 +- .../destinations/amplitude/mapUser/index.ts | 4 +- 8 files changed, 77 insertions(+), 113 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts diff --git a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts deleted file mode 100644 index a401d656f8d..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/autocapture-fields.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { InputField } from '@segment/actions-core' -import { DESTINATION_INTEGRATION_NAME } from './events-constants' -export const autocapture_fields: Record = { - autocaptureAttributionEnabled: { - label: 'Autocapture Attribution Enabled', - description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', - type: 'boolean', - default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, - readOnly: true - }, - autocaptureAttributionSet: { - label: 'Autocapture Attribution Set', - description: 'Utility field used to detect if any attribution values need to be set.', - type: 'object', - default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, - readOnly: true - }, - autocaptureAttributionSetOnce: { - label: 'Autocapture Attribution Set Once', - description: 'Utility field used to detect if any attribution values need to be set_once.', - type: 'object', - default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, - readOnly: true - }, - autocaptureAttributionUnset: { - label: 'Autocapture Attribution Unset', - description: 'Utility field used to detect if any attribution values need to be unset.', - type: 'object', - default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, - readOnly: true - } -} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts index d7e575a1eac..21cce2f08a8 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -3,9 +3,9 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import dayjs from '../../../lib/dayjs' import { getEndpointByRegion } from '../common-functions' -import { common_fields } from '../common-fields' +import { common_fields } from '../fields/common-fields' import { group_properties, group_type, group_value } from './fields' -import { time } from '../misc-fields' +import { time } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Group Identify User', diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts index 74859617113..ecf23bae63d 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts @@ -2,11 +2,11 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { send } from './functions' -import { autocapture_fields } from '../autocapture-fields' -import { common_fields } from '../common-fields' -import { common_track_identify_fields} from '../common-track-identify-fields' +import { autocapture_fields } from '../fields/autocapture-fields' +import { common_fields } from '../fields/common-fields' +import { common_track_identify_fields} from '../fields/common-track-identify-fields' import { paying, start_version } from './fields' -import { min_id_length } from '../misc-fields' +import { min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Identify User', diff --git a/packages/destination-actions/src/destinations/amplitude/index.ts b/packages/destination-actions/src/destinations/amplitude/index.ts index b2490cd661e..a3a3251cae0 100644 --- a/packages/destination-actions/src/destinations/amplitude/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/index.ts @@ -7,65 +7,8 @@ import groupIdentifyUser from './groupIdentifyUser' import logPurchase from './logPurchase' import type { Settings } from './generated-types' import { getEndpointByRegion } from './common-functions' - import logEventV2 from './logEventV2' -/** used in the quick setup */ -const presets: DestinationDefinition['presets'] = [ - { - name: 'Track Calls', - subscribe: 'type = "track" and event != "Order Completed"', - partnerAction: 'logEventV2', - mapping: defaultValues(logEventV2.fields), - type: 'automatic' - }, - { - name: 'Order Completed Calls', - subscribe: 'type = "track" and event = "Order Completed"', - partnerAction: 'logPurchase', - mapping: defaultValues(logPurchase.fields), - type: 'automatic' - }, - { - name: 'Page Calls', - subscribe: 'type = "page"', - partnerAction: 'logEventV2', - mapping: { - ...defaultValues(logEventV2.fields), - event_type: { - '@template': 'Viewed {{name}}' - } - }, - type: 'automatic' - }, - { - name: 'Screen Calls', - subscribe: 'type = "screen"', - partnerAction: 'logEventV2', - mapping: { - ...defaultValues(logEventV2.fields), - event_type: { - '@template': 'Viewed {{name}}' - } - }, - type: 'automatic' - }, - { - name: 'Identify Calls', - subscribe: 'type = "identify"', - partnerAction: 'identifyUser', - mapping: defaultValues(identifyUser.fields), - type: 'automatic' - }, - { - name: 'Browser Session Tracking', - subscribe: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', - partnerAction: 'sessionId', - mapping: {}, - type: 'automatic' - } -] - const destination: DestinationDefinition = { name: 'Actions Amplitude', slug: 'actions-amplitude', @@ -125,7 +68,60 @@ const destination: DestinationDefinition = { } }) }, - presets, + presets: [ + { + name: 'Track Calls', + subscribe: 'type = "track" and event != "Order Completed"', + partnerAction: 'logEventV2', + mapping: defaultValues(logEventV2.fields), + type: 'automatic' + }, + { + name: 'Order Completed Calls', + subscribe: 'type = "track" and event = "Order Completed"', + partnerAction: 'logPurchase', + mapping: defaultValues(logPurchase.fields), + type: 'automatic' + }, + { + name: 'Page Calls', + subscribe: 'type = "page"', + partnerAction: 'logEventV2', + mapping: { + ...defaultValues(logEventV2.fields), + event_type: { + '@template': 'Viewed {{name}}' + } + }, + type: 'automatic' + }, + { + name: 'Screen Calls', + subscribe: 'type = "screen"', + partnerAction: 'logEventV2', + mapping: { + ...defaultValues(logEventV2.fields), + event_type: { + '@template': 'Viewed {{name}}' + } + }, + type: 'automatic' + }, + { + name: 'Identify Calls', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: defaultValues(identifyUser.fields), + type: 'automatic' + }, + { + name: 'Browser Session Tracking', + subscribe: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"', + partnerAction: 'sessionId', + mapping: {}, + type: 'automatic' + } + ], actions: { logEvent, identifyUser, diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index a642d80e2ef..06b791a6319 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -2,11 +2,11 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { send } from '../events-functions' -import { common_fields } from '../common-fields' -import { common_track_fields } from '../common-track-fields' -import { common_track_identify_fields } from '../common-track-identify-fields' -import { autocapture_fields } from '../autocapture-fields' -import { min_id_length } from '../misc-fields' +import { common_fields } from '../fields/common-fields' +import { common_track_fields } from '../fields/common-track-fields' +import { common_track_identify_fields } from '../fields/common-track-identify-fields' +import { autocapture_fields } from '../fields/autocapture-fields' +import { min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Log Event', diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 695cd42867e..487504d4a27 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,13 +1,13 @@ import { ActionDefinition} from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { autocapture_fields } from '../autocapture-fields' +import { autocapture_fields } from '../fields/autocapture-fields' import { send } from '../events-functions' -import { common_fields } from '../common-fields' -import { common_track_fields } from '../common-track-fields' -import { common_track_identify_fields } from '../common-track-identify-fields' +import { common_fields } from '../fields/common-fields' +import { common_track_fields } from '../fields/common-track-fields' +import { common_track_identify_fields } from '../fields/common-track-identify-fields' import { trackRevenuePerProduct, setOnce, setAlways, add } from './fields' -import { min_id_length } from '../misc-fields' +import { min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Log Event V2', diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index bcedd48cd29..15ed90bc02f 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -2,12 +2,12 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { send } from '../events-functions' -import { common_fields } from '../common-fields' -import { common_track_fields } from '../common-track-fields' -import { common_track_identify_fields } from '../common-track-identify-fields' -import { autocapture_fields } from '../autocapture-fields' +import { common_fields } from '../fields/common-fields' +import { common_track_fields } from '../fields/common-track-fields' +import { common_track_identify_fields } from '../fields/common-track-identify-fields' +import { autocapture_fields } from '../fields/autocapture-fields' import { trackRevenuePerProduct } from './fields' -import { min_id_length } from '../misc-fields' +import { min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Log Purchase', diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index 4ce1cfec7f7..0d0be7a5eb6 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -2,9 +2,9 @@ import type { ActionDefinition } from '@segment/actions-core' import { getEndpointByRegion } from '../common-functions' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { user_id } from '../common-fields' +import { user_id } from '../fields/common-fields' import { global_user_id } from './fields' -import { min_id_length } from '../misc-fields' +import { min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Map User', From e1f1fd56fb0870fa221e73610892f750be8def73 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 12:43:47 +0000 Subject: [PATCH 36/38] refactor - tests passing --- .../amplitude/__tests__/amplitude.test.ts | 18 +- .../__tests__/regional-endpoints.test.ts | 2 +- .../amplitude/fields/autocapture-fields.ts | 32 ++ .../amplitude/fields/common-fields.ts | 15 + .../amplitude/fields/common-track-fields.ts | 259 +++++++++++++++++ .../fields/common-track-identify-fields.ts | 218 ++++++++++++++ .../amplitude/fields/misc-fields.ts | 82 ++++++ .../groupIdentifyUser/generated-types.ts | 12 +- .../amplitude/groupIdentifyUser/index.ts | 4 +- .../amplitude/identifyUser/generated-types.ts | 150 +++++----- .../amplitude/identifyUser/index.ts | 9 +- .../amplitude/logEvent/generated-types.ts | 242 ++++++++-------- .../destinations/amplitude/logEvent/index.ts | 9 +- .../amplitude/logEventV2/generated-types.ts | 274 +++++++++--------- .../amplitude/logEventV2/index.ts | 7 +- .../amplitude/logPurchase/generated-types.ts | 242 ++++++++-------- .../amplitude/logPurchase/index.ts | 9 +- .../destinations/amplitude/mapUser/index.ts | 7 +- 18 files changed, 1098 insertions(+), 493 deletions(-) create mode 100644 packages/destination-actions/src/destinations/amplitude/fields/autocapture-fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/fields/common-fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/fields/common-track-fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/fields/common-track-identify-fields.ts create mode 100644 packages/destination-actions/src/destinations/amplitude/fields/misc-fields.ts diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts index 610333de36e..5c558a2cdda 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts @@ -1959,7 +1959,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22os_name%22%3A%22iOS%22%2C%22os_version%22%3A%229%22%2C%22device_manufacturer%22%3A%22Apple%22%2C%22device_model%22%3A%22iPhone%22%2C%22device_type%22%3A%22mobile%22%2C%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22country%22%3A%22United+States%22%2C%22city%22%3A%22San+Francisco%22%2C%22language%22%3A%22en-US%22%2C%22platform%22%3A%22Web%22%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22os_name%22%3A%22iOS%22%2C%22os_version%22%3A%229%22%2C%22device_manufacturer%22%3A%22Apple%22%2C%22device_model%22%3A%22iPhone%22%2C%22device_type%22%3A%22mobile%22%2C%22city%22%3A%22San+Francisco%22%2C%22country%22%3A%22United+States%22%2C%22language%22%3A%22en-US%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_id%22%3A%22some-user-id%22%2C%22platform%22%3A%22Web%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -1992,7 +1992,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%2C%22%24set%22%3A%7B%22utm_source%22%3A%22Newsletter%22%2C%22utm_medium%22%3A%22email%22%2C%22utm_campaign%22%3A%22TPS+Innovation+Newsletter%22%2C%22utm_term%22%3A%22tps+reports%22%2C%22utm_content%22%3A%22image+link%22%2C%22referrer%22%3A%22some-referrer%22%7D%2C%22%24setOnce%22%3A%7B%22initial_utm_source%22%3A%22Newsletter%22%2C%22initial_utm_medium%22%3A%22email%22%2C%22initial_utm_campaign%22%3A%22TPS+Innovation+Newsletter%22%2C%22initial_utm_term%22%3A%22tps+reports%22%2C%22initial_utm_content%22%3A%22image+link%22%2C%22initial_referrer%22%3A%22some-referrer%22%7D%7D%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%2C%22%24setOnce%22%3A%7B%22initial_referrer%22%3A%22some-referrer%22%2C%22initial_utm_source%22%3A%22Newsletter%22%2C%22initial_utm_medium%22%3A%22email%22%2C%22initial_utm_campaign%22%3A%22TPS+Innovation+Newsletter%22%2C%22initial_utm_term%22%3A%22tps+reports%22%2C%22initial_utm_content%22%3A%22image+link%22%7D%2C%22%24set%22%3A%7B%22referrer%22%3A%22some-referrer%22%2C%22utm_source%22%3A%22Newsletter%22%2C%22utm_medium%22%3A%22email%22%2C%22utm_campaign%22%3A%22TPS+Innovation+Newsletter%22%2C%22utm_term%22%3A%22tps+reports%22%2C%22utm_content%22%3A%22image+link%22%7D%7D%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_id%22%3A%22some-user-id%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2013,7 +2013,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22os_name%22%3A%22iOS%22%2C%22os_version%22%3A%229%22%2C%22device_manufacturer%22%3A%22Apple%22%2C%22device_model%22%3A%22iPhone%22%2C%22device_type%22%3A%22mobile%22%2C%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22country%22%3A%22United+States%22%2C%22city%22%3A%22San+Francisco%22%2C%22language%22%3A%22en-US%22%2C%22platform%22%3A%22Web%22%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22os_name%22%3A%22iOS%22%2C%22os_version%22%3A%229%22%2C%22device_manufacturer%22%3A%22Apple%22%2C%22device_model%22%3A%22iPhone%22%2C%22device_type%22%3A%22mobile%22%2C%22city%22%3A%22San+Francisco%22%2C%22country%22%3A%22United+States%22%2C%22language%22%3A%22en-US%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_id%22%3A%22some-user-id%22%2C%22platform%22%3A%22Web%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2046,7 +2046,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22os_name%22%3A%22Mac+OS%22%2C%22os_version%22%3A%2253%22%2C%22device_model%22%3A%22Mac+OS%22%2C%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22foo%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22os_name%22%3A%22Mac+OS%22%2C%22os_version%22%3A%2253%22%2C%22device_model%22%3A%22Mac+OS%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22device_id%22%3A%22foo%22%2C%22user_id%22%3A%22some-user-id%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2079,7 +2079,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22foo%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22device_id%22%3A%22foo%22%2C%22user_id%22%3A%22some-user-id%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2100,7 +2100,7 @@ describe('Amplitude', () => { nock('https://api2.amplitude.com').post('/identify').reply(200, {}) const responses = await testDestination.testAction('identifyUser', { event, mapping, useDefaultMappings: true }) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22user_id%22%3A%22user1234%22%2C%22device_id%22%3A%22foo%22%2C%22user_properties%22%3A%7B%7D%2C%22platform%22%3A%22Android%22%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22user_properties%22%3A%7B%7D%2C%22device_id%22%3A%22foo%22%2C%22user_id%22%3A%22user1234%22%2C%22platform%22%3A%22Android%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2121,7 +2121,7 @@ describe('Amplitude', () => { nock('https://api2.amplitude.com').post('/identify').reply(200, {}) const responses = await testDestination.testAction('identifyUser', { event, mapping, useDefaultMappings: true }) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22user_id%22%3A%22user1234%22%2C%22device_id%22%3A%22foo%22%2C%22user_properties%22%3A%7B%7D%2C%22platform%22%3A%22iOS%22%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22user_properties%22%3A%7B%7D%2C%22device_id%22%3A%22foo%22%2C%22user_id%22%3A%22user1234%22%2C%22platform%22%3A%22iOS%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2150,7 +2150,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=&identification=%7B%22os_name%22%3A%22iOS%22%2C%22os_version%22%3A%229%22%2C%22device_manufacturer%22%3A%22Apple%22%2C%22device_model%22%3A%22iPhone%22%2C%22device_type%22%3A%22mobile%22%2C%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22country%22%3A%22United+States%22%2C%22city%22%3A%22San+Francisco%22%2C%22language%22%3A%22en-US%22%2C%22platform%22%3A%22Web%22%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=&identification=%7B%22os_name%22%3A%22iOS%22%2C%22os_version%22%3A%229%22%2C%22device_manufacturer%22%3A%22Apple%22%2C%22device_model%22%3A%22iPhone%22%2C%22device_type%22%3A%22mobile%22%2C%22city%22%3A%22San+Francisco%22%2C%22country%22%3A%22United+States%22%2C%22language%22%3A%22en-US%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22device_id%22%3A%22some-anonymous-id%22%2C%22user_id%22%3A%22some-user-id%22%2C%22platform%22%3A%22Web%22%2C%22library%22%3A%22segment%22%7D"` ) }) @@ -2187,7 +2187,7 @@ describe('Amplitude', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) expect(responses[0].options.body?.toString()).toMatchInlineSnapshot( - `"api_key=undefined&identification=%7B%22os_name%22%3A%22iPhone+OS%22%2C%22os_version%22%3A%228.1.3%22%2C%22device_model%22%3A%22Mac+OS%22%2C%22user_id%22%3A%22some-user-id%22%2C%22device_id%22%3A%22foo%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22library%22%3A%22segment%22%7D&options=undefined"` + `"api_key=undefined&identification=%7B%22os_name%22%3A%22iPhone+OS%22%2C%22os_version%22%3A%228.1.3%22%2C%22device_model%22%3A%22Mac+OS%22%2C%22user_properties%22%3A%7B%22some-trait-key%22%3A%22some-trait-value%22%7D%2C%22device_id%22%3A%22foo%22%2C%22user_id%22%3A%22some-user-id%22%2C%22library%22%3A%22segment%22%7D"` ) }) }) diff --git a/packages/destination-actions/src/destinations/amplitude/__tests__/regional-endpoints.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/regional-endpoints.test.ts index ab900aa966d..b352e5758eb 100644 --- a/packages/destination-actions/src/destinations/amplitude/__tests__/regional-endpoints.test.ts +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/regional-endpoints.test.ts @@ -1,4 +1,4 @@ -import { endpoints, getEndpointByRegion } from '../regional-endpoints' +import { endpoints, getEndpointByRegion } from '../common-functions' describe('Amplitude - Regional endpoints', () => { it('should set region to north_america when no region is provided', () => { diff --git a/packages/destination-actions/src/destinations/amplitude/fields/autocapture-fields.ts b/packages/destination-actions/src/destinations/amplitude/fields/autocapture-fields.ts new file mode 100644 index 00000000000..13c1ce6d314 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/fields/autocapture-fields.ts @@ -0,0 +1,32 @@ +import type { InputField } from '@segment/actions-core' +import { DESTINATION_INTEGRATION_NAME } from '../events-constants' +export const autocapture_fields: Record = { + autocaptureAttributionEnabled: { + label: 'Autocapture Attribution Enabled', + description: 'Utility field used to detect if Autocapture Attribution Plugin is enabled.', + type: 'boolean', + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.enabled` }, + readOnly: true + }, + autocaptureAttributionSet: { + label: 'Autocapture Attribution Set', + description: 'Utility field used to detect if any attribution values need to be set.', + type: 'object', + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set` }, + readOnly: true + }, + autocaptureAttributionSetOnce: { + label: 'Autocapture Attribution Set Once', + description: 'Utility field used to detect if any attribution values need to be set_once.', + type: 'object', + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.set_once` }, + readOnly: true + }, + autocaptureAttributionUnset: { + label: 'Autocapture Attribution Unset', + description: 'Utility field used to detect if any attribution values need to be unset.', + type: 'object', + default: { '@path': `$.integrations.${DESTINATION_INTEGRATION_NAME}.autocapture_attribution.unset` }, + readOnly: true + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/fields/common-fields.ts b/packages/destination-actions/src/destinations/amplitude/fields/common-fields.ts new file mode 100644 index 00000000000..98c38af3e3f --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/fields/common-fields.ts @@ -0,0 +1,15 @@ +import { InputField } from '@segment/actions-core' + +export const user_id: InputField = { + label: 'User ID', + type: 'string', + allowNull: true, + description: 'A readable ID specified by you. Must have a minimum length of 5 characters. Required unless device ID is present. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event.', + default: { + '@path': '$.userId' + } +} + +export const common_fields = { + user_id +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/fields/common-track-fields.ts b/packages/destination-actions/src/destinations/amplitude/fields/common-track-fields.ts new file mode 100644 index 00000000000..f61b871bcc8 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/fields/common-track-fields.ts @@ -0,0 +1,259 @@ +import { InputField } from '@segment/actions-core' + +export const adid: InputField = { + label: 'Google Play Services Advertising ID', + type: 'string', + description: 'Google Play Services advertising ID. _(Android)_', + default: { + '@if': { + exists: { '@path': '$.context.device.advertisingId' }, + then: { '@path': '$.context.device.advertisingId' }, + else: { '@path': '$.context.device.idfa' } + } + } +} + +export const android_id: InputField = { + label: 'Android ID', + type: 'string', + description: 'Android ID (not the advertising ID). _(Android)_' +} + +export const event_id: InputField = { + label: 'Event ID', + type: 'integer', + description: + 'An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously.' +} + +export const event_properties: InputField = { + label: 'Event Properties', + type: 'object', + description: + 'An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', + default: { + '@path': '$.properties' + } +} + +export const event_type: InputField = { + label: 'Event Type', + type: 'string', + description: 'A unique identifier for your event.', + required: true, + default: { + '@path': '$.event' + } +} + +export const idfa: InputField = { + label: 'Identifier For Advertiser (IDFA)', + type: 'string', + description: 'Identifier for Advertiser. _(iOS)_', + default: { + '@if': { + exists: { '@path': '$.context.device.advertisingId' }, + then: { '@path': '$.context.device.advertisingId' }, + else: { '@path': '$.context.device.idfa' } + } + } +} + +export const idfv: InputField = { + label: 'Identifier For Vendor (IDFV)', + type: 'string', + description: 'Identifier for Vendor. _(iOS)_', + default: { + '@path': '$.context.device.id' + } +} + +export const ip: InputField = { + label: 'IP Address', + type: 'string', + description: + 'The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user\'s location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers.', + default: { + '@path': '$.context.ip' + } +} + +export const location_lat: InputField = { + label: 'Latitude', + type: 'number', + description: 'The current Latitude of the user.', + default: { + '@path': '$.context.location.latitude' + } +} + +export const location_lng: InputField = { + label: 'Longtitude', + type: 'number', + description: 'The current Longitude of the user.', + default: { + '@path': '$.context.location.longitude' + } +} + +export const price: InputField = { + label: 'Price', + type: 'number', + description: + 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.', + default: { + '@path': '$.properties.price' + } +} + +export const productId: InputField = { + label: 'Product ID', + type: 'string', + description: + 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.', + default: { + '@path': '$.properties.productId' + } +} + +export const products: InputField = { + label: 'Products', + description: 'The list of products purchased.', + type: 'object', + multiple: true, + additionalProperties: true, + properties: { + price: { + label: 'Price', + type: 'number', + description: + 'The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds.' + }, + quantity: { + label: 'Quantity', + type: 'integer', + description: 'The quantity of the item purchased. Defaults to 1 if not specified.' + }, + revenue: { + label: 'Revenue', + type: 'number', + description: + 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds.' + }, + productId: { + label: 'Product ID', + type: 'string', + description: + 'An identifier for the item purchased. You must send a price and quantity or revenue with this field.' + }, + revenueType: { + label: 'Revenue Type', + type: 'string', + description: + 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.' + } + }, + default: { + '@arrayPath': [ + '$.properties.products', + { + price: { + '@path': 'price' + }, + revenue: { + '@path': 'revenue' + }, + quantity: { + '@path': 'quantity' + }, + productId: { + '@path': 'productId' + }, + revenueType: { + '@path': 'revenueType' + } + } + ] + } +} + +export const quantity: InputField = { + label: 'Quantity', + type: 'integer', + description: 'The quantity of the item purchased. Defaults to 1 if not specified.', + default: { + '@path': '$.properties.quantity' + } +} + +export const revenue: InputField = { + label: 'Revenue', + type: 'number', + description: + 'Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode.', + default: { + '@path': '$.properties.revenue' + } +} + +export const revenueType: InputField = { + label: 'Revenue Type', + type: 'string', + description: + 'The type of revenue for the item purchased. You must send a price and quantity or revenue with this field.', + default: { + '@path': '$.properties.revenueType' + } +} + +export const session_id: InputField = { + label: 'Session ID', + type: 'datetime', + description: 'The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source.', + default: { + '@path': '$.integrations.Actions Amplitude.session_id' + } +} + +export const use_batch_endpoint: InputField ={ + label: 'Use Batch Endpoint', + description: + "If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth).", + type: 'boolean', + default: false +} + +export const common_track_fields = { + adid, + android_id, + event_id, + event_properties, + event_type, + idfa, + idfv, + ip, + location_lat, + location_lng, + price, + productId, + products, + quantity, + revenue, + revenueType, + session_id, + use_batch_endpoint +} + + + + + + + + + + + + + + diff --git a/packages/destination-actions/src/destinations/amplitude/fields/common-track-identify-fields.ts b/packages/destination-actions/src/destinations/amplitude/fields/common-track-identify-fields.ts new file mode 100644 index 00000000000..3fc57aede5f --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/fields/common-track-identify-fields.ts @@ -0,0 +1,218 @@ +import { InputField } from '@segment/actions-core' + +export const app_version: InputField = { + label: 'App Version', + type: 'string', + description: 'The current version of your application.', + default: { + '@path': '$.context.app.version' + } +} + +export const carrier: InputField = { + label: 'Carrier', + type: 'string', + description: 'The carrier that the user is using.', + default: { + '@path': '$.context.network.carrier' + } +} + +export const city: InputField = { + label: 'City', + type: 'string', + description: 'The current city of the user.', + default: { + '@path': '$.context.location.city' + } +} + +export const country: InputField = { + label: 'Country', + type: 'string', + description: 'The current country of the user.', + default: { + '@path': '$.context.location.country' + } +} + +export const region: InputField = { + label: 'Region', + type: 'string', + description: 'The current region of the user.', + default: { + '@path': '$.context.location.region' + } +} + +export const device_brand: InputField = { + label: 'Device Brand', + type: 'string', + description: 'The device brand that the user is using.', + default: { + '@path': '$.context.device.brand' + } +} + +export const device_manufacturer: InputField = { + label: 'Device Manufacturer', + type: 'string', + description: 'The device manufacturer that the user is using.', + default: { + '@path': '$.context.device.manufacturer' + } +}; + +export const device_model: InputField = { + label: 'Device Model', + type: 'string', + description: 'The device model that the user is using.', + default: { + '@path': '$.context.device.model' + } +} + +export const dma: InputField = { + label: 'Designated Market Area', + type: 'string', + description: 'The current Designated Market Area of the user.' +} + +export const groups: InputField = { + label: 'Groups', + type: 'object', + description: + 'Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on.' +} + +export const includeRawUserAgent: InputField ={ + label: 'Include Raw User Agent', + type: 'boolean', + description: + 'Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field', + default: false +} + +export const language: InputField = { + label: 'Language', + type: 'string', + description: 'The language set by the user.', + default: { + '@path': '$.context.locale' + } +} + +export const library: InputField = { + label: 'Library', + type: 'string', + description: 'The name of the library that generated the event.', + default: { + '@path': '$.context.library.name' + } +} + +export const os_name: InputField = { + label: 'OS Name', + type: 'string', + description: 'The name of the mobile operating system or browser that the user is using.', + default: { + '@path': '$.context.os.name' + } +} + +export const os_version: InputField = { + label: 'OS Version', + type: 'string', + description: 'The version of the mobile operating system or browser the user is using.', + default: { + '@path': '$.context.os.version' + } +}; + +export const platform: InputField = { + label: 'Platform', + type: 'string', + description: + 'Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent.', + default: { + '@path': '$.context.device.type' + } +} + +export const userAgent: InputField ={ + label: 'User Agent', + type: 'string', + description: 'The user agent of the device sending the event.', + default: { + '@path': '$.context.userAgent' + } +} + +export const userAgentData: InputField = { + label: 'User Agent Data', + type: 'object', + description: 'The user agent data of device sending the event', + properties: { + model: { + label: 'Model', + type: 'string' + }, + platformVersion: { + label: 'PlatformVersion', + type: 'string' + } + }, + default: { + model: { '@path': '$.context.userAgentData.model' }, + platformVersion: { '@path': '$.context.userAgentData.platformVersion' } + } +} + +export const userAgentParsing: InputField = { + label: 'User Agent Parsing', + type: 'boolean', + description: + 'Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field', + default: true +} + + + +export const user_properties: InputField = { + label: 'User Properties', + type: 'object', + description: + 'An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers.', + default: { + '@path': '$.traits' + } +} + +export const common_track_identify_fields = { + app_version, + carrier, + city, + country, + region, + device_brand, + device_manufacturer, + device_model, + dma, + groups, + includeRawUserAgent, + language, + library, + os_name, + os_version, + platform, + userAgent, + userAgentData, + userAgentParsing, + user_properties +} + + + + + + diff --git a/packages/destination-actions/src/destinations/amplitude/fields/misc-fields.ts b/packages/destination-actions/src/destinations/amplitude/fields/misc-fields.ts new file mode 100644 index 00000000000..2f813c4a391 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/fields/misc-fields.ts @@ -0,0 +1,82 @@ +import { InputField } from '@segment/actions-core' + +export const time: InputField = { + label: 'Timestamp', + type: 'datetime', + description: 'The timestamp of the event. If time is not sent with the event, it will be set to the request upload time.', + default: { + '@path': '$.timestamp' + } +} + +export const min_id_length: InputField = { + label: 'Minimum ID Length', + description: 'Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths.', + allowNull: true, + type: 'integer' +} + +export const device_id: InputField = { + label: 'Device ID', + type: 'string', + description: 'A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID.', + default: { + '@if': { + exists: { '@path': '$.context.device.id' }, + then: { '@path': '$.context.device.id' }, + else: { '@path': '$.anonymousId' } + } + } +} + +export const insert_id: InputField = { + label: 'Insert ID', + type: 'string', + description: + 'Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time.' +} + +export const utm_properties: InputField = { + label: 'UTM Properties', + type: 'object', + description: 'UTM Tracking Properties', + properties: { + utm_source: { + label: 'UTM Source', + type: 'string' + }, + utm_medium: { + label: 'UTM Medium', + type: 'string' + }, + utm_campaign: { + label: 'UTM Campaign', + type: 'string' + }, + utm_term: { + label: 'UTM Term', + type: 'string' + }, + utm_content: { + label: 'UTM Content', + type: 'string' + } + }, + default: { + utm_source: { '@path': '$.context.campaign.source' }, + utm_medium: { '@path': '$.context.campaign.medium' }, + utm_campaign: { '@path': '$.context.campaign.name' }, + utm_term: { '@path': '$.context.campaign.term' }, + utm_content: { '@path': '$.context.campaign.content' } + } +} + +export const referrer: InputField = { + label: 'Referrer', + type: 'string', + description: + 'The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer”', + default: { + '@path': '$.context.page.referrer' + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts index 637c234de56..a326b669d5c 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts @@ -6,17 +6,17 @@ export interface Payload { */ user_id?: string | null /** - * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. + * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ - device_id?: string + time?: string | number /** * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. */ insert_id?: string /** - * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. + * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. */ - time?: string | number + device_id?: string /** * Additional data tied to the group in Amplitude. */ @@ -31,8 +31,4 @@ export interface Payload { * The value of the group */ group_value: string - /** - * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. - */ - min_id_length?: number | null } diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts index 21cce2f08a8..8f3d89acc3d 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -5,7 +5,7 @@ import dayjs from '../../../lib/dayjs' import { getEndpointByRegion } from '../common-functions' import { common_fields } from '../fields/common-fields' import { group_properties, group_type, group_value } from './fields' -import { time } from '../fields/misc-fields' +import { device_id, time, insert_id } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Group Identify User', @@ -19,6 +19,8 @@ const action: ActionDefinition = { description: 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. If either user ID or device ID is present, an associate user to group call will be made.' }, time, + insert_id, + device_id, group_properties, group_type, group_value diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts index d746e71424f..9e6d55558a2 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts @@ -2,41 +2,45 @@ export interface Payload { /** - * Utility field used to detect if Autocapture Attribution Plugin is enabled. + * A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present. */ - autocaptureAttributionEnabled?: boolean + user_id?: string | null /** - * Utility field used to detect if any attribution values need to be set. + * The current version of your application. */ - autocaptureAttributionSet?: { - [k: string]: unknown - } + app_version?: string /** - * Utility field used to detect if any attribution values need to be set_once. + * The carrier that the user is using. */ - autocaptureAttributionSetOnce?: { - [k: string]: unknown - } + carrier?: string /** - * Utility field used to detect if any attribution values need to be unset. + * The current city of the user. */ - autocaptureAttributionUnset?: { - [k: string]: unknown - } + city?: string /** - * A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present. + * The current country of the user. */ - user_id?: string | null + country?: string /** - * A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present. + * The current region of the user. */ - device_id?: string + region?: string /** - * Additional data tied to the user in Amplitude. Each distinct value will show up as a user segment on the Amplitude dashboard. Object depth may not exceed 40 layers. **Note:** You can store property values in an array and date values are transformed into string values. + * The device brand that the user is using. */ - user_properties?: { - [k: string]: unknown - } + device_brand?: string + /** + * The device manufacturer that the user is using. + */ + device_manufacturer?: string + /** + * The device model that the user is using. + */ + device_model?: string + /** + * The current Designated Market Area of the user. + */ + dma?: string /** * Groups of users for Amplitude's account-level reporting feature. Note: You can only track up to 5 groups. Any groups past that threshold will not be tracked. **Note:** This feature is only available to Amplitude Enterprise customers who have purchased the Amplitude Accounts add-on. */ @@ -44,13 +48,17 @@ export interface Payload { [k: string]: unknown } /** - * The current version of your application. + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - app_version?: string + includeRawUserAgent?: boolean /** - * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. + * The language set by the user. */ - platform?: string + language?: string + /** + * The name of the library that generated the event. + */ + library?: string /** * The name of the mobile operating system or browser that the user is using. */ @@ -60,65 +68,76 @@ export interface Payload { */ os_version?: string /** - * The device brand that the user is using. - */ - device_brand?: string - /** - * The device manufacturer that the user is using. + * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. */ - device_manufacturer?: string + platform?: string /** - * The device model that the user is using. + * The user agent of the device sending the event. */ - device_model?: string + userAgent?: string /** - * The carrier that the user is using. + * The user agent data of device sending the event */ - carrier?: string + userAgentData?: { + model?: string + platformVersion?: string + } /** - * The current country of the user. + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field */ - country?: string + userAgentParsing?: boolean /** - * The current region of the user. + * Additional data tied to the user in Amplitude. Each distinct value will show up as a user segment on the Amplitude dashboard. Object depth may not exceed 40 layers. **Note:** You can store property values in an array and date values are transformed into string values. */ - region?: string + user_properties?: { + [k: string]: unknown + } /** - * The current city of the user. + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - city?: string + autocaptureAttributionEnabled?: boolean /** - * The current Designated Market Area of the user. + * Utility field used to detect if any attribution values need to be set. */ - dma?: string + autocaptureAttributionSet?: { + [k: string]: unknown + } /** - * The language set by the user. + * Utility field used to detect if any attribution values need to be set_once. */ - language?: string + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } /** - * Whether the user is paying or not. + * Utility field used to detect if any attribution values need to be unset. */ - paying?: boolean + autocaptureAttributionUnset?: { + [k: string]: unknown + } /** - * The version of the app the user was first on. + * A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present. */ - start_version?: string + device_id?: string /** * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. */ insert_id?: string /** - * The user agent of the device sending the event. + * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ - userAgent?: string + min_id_length?: number | null /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + * Whether the user is paying or not. */ - userAgentParsing?: boolean + paying?: boolean /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * The version of the app the user was first on. */ - includeRawUserAgent?: boolean + start_version?: string + /** + * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” + */ + referrer?: string /** * UTM Tracking Properties */ @@ -129,23 +148,4 @@ export interface Payload { utm_term?: string utm_content?: string } - /** - * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” - */ - referrer?: string - /** - * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. - */ - min_id_length?: number | null - /** - * The name of the library that generated the event. - */ - library?: string - /** - * The user agent data of device sending the event - */ - userAgentData?: { - model?: string - platformVersion?: string - } } diff --git a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts index ecf23bae63d..239a204ae93 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts @@ -6,7 +6,7 @@ import { autocapture_fields } from '../fields/autocapture-fields' import { common_fields } from '../fields/common-fields' import { common_track_identify_fields} from '../fields/common-track-identify-fields' import { paying, start_version } from './fields' -import { min_id_length } from '../fields/misc-fields' +import { min_id_length, device_id, insert_id, referrer, utm_properties } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Identify User', @@ -22,9 +22,10 @@ const action: ActionDefinition = { description: 'A UUID (unique user ID) specified by you. **Note:** If you send a request with a user ID that is not in the Amplitude system yet, then the user tied to that ID will not be marked new until their first event. Required unless device ID is present.' }, device_id: { - ...common_track_identify_fields.device_id, + ...device_id, description: 'A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present.' }, + insert_id, user_properties: { ...common_track_identify_fields.user_properties, description: 'Additional data tied to the user in Amplitude. Each distinct value will show up as a user segment on the Amplitude dashboard. Object depth may not exceed 40 layers. **Note:** You can store property values in an array and date values are transformed into string values.' @@ -35,7 +36,9 @@ const action: ActionDefinition = { }, min_id_length, paying, - start_version + start_version, + referrer, + utm_properties }, perform: (request, { payload, settings }) => { return send(request, payload, settings) diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts index 3c90665ca5b..49cc33c52a1 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts @@ -6,21 +6,17 @@ export interface Payload { */ user_id?: string | null /** - * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. - */ - device_id?: string - /** - * A unique identifier for your event. + * Google Play Services advertising ID. _(Android)_ */ - event_type: string + adid?: string /** - * The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source. + * Android ID (not the advertising ID). _(Android)_ */ - session_id?: string | number + android_id?: string /** - * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. + * An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously. */ - time?: string | number + event_id?: number /** * An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. */ @@ -28,155 +24,170 @@ export interface Payload { [k: string]: unknown } /** - * An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + * A unique identifier for your event. */ - user_properties?: { - [k: string]: unknown - } + event_type: string /** - * Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on. + * Identifier for Advertiser. _(iOS)_ */ - groups?: { - [k: string]: unknown - } + idfa?: string /** - * The current version of your application. + * Identifier for Vendor. _(iOS)_ */ - app_version?: string + idfv?: string /** - * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. + * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. */ - platform?: string + ip?: string /** - * The name of the mobile operating system or browser that the user is using. + * The current Latitude of the user. */ - os_name?: string + location_lat?: number /** - * The version of the mobile operating system or browser the user is using. + * The current Longitude of the user. */ - os_version?: string + location_lng?: number /** - * The device brand that the user is using. + * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. */ - device_brand?: string + price?: number /** - * The device manufacturer that the user is using. + * An identifier for the item purchased. You must send a price and quantity or revenue with this field. */ - device_manufacturer?: string + productId?: string /** - * The device model that the user is using. + * The list of products purchased. */ - device_model?: string + products?: { + /** + * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. + */ + price?: number + /** + * The quantity of the item purchased. Defaults to 1 if not specified. + */ + quantity?: number + /** + * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. + */ + revenue?: number + /** + * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + */ + productId?: string + /** + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + */ + revenueType?: string + [k: string]: unknown + }[] /** - * The carrier that the user is using. + * The quantity of the item purchased. Defaults to 1 if not specified. */ - carrier?: string + quantity?: number /** - * The current country of the user. + * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. */ - country?: string + revenue?: number /** - * The current region of the user. + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. */ - region?: string + revenueType?: string /** - * The current city of the user. + * The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source. */ - city?: string + session_id?: string | number /** - * The current Designated Market Area of the user. + * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ - dma?: string + use_batch_endpoint?: boolean /** - * The language set by the user. + * The current version of your application. */ - language?: string + app_version?: string /** - * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. + * The carrier that the user is using. */ - price?: number + carrier?: string /** - * The quantity of the item purchased. Defaults to 1 if not specified. + * The current city of the user. */ - quantity?: number + city?: string /** - * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. + * The current country of the user. */ - revenue?: number + country?: string /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + * The current region of the user. */ - productId?: string + region?: string /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + * The device brand that the user is using. */ - revenueType?: string + device_brand?: string /** - * The current Latitude of the user. + * The device manufacturer that the user is using. */ - location_lat?: number + device_manufacturer?: string /** - * The current Longitude of the user. + * The device model that the user is using. */ - location_lng?: number + device_model?: string /** - * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. + * The current Designated Market Area of the user. */ - ip?: string + dma?: string /** - * Identifier for Advertiser. _(iOS)_ + * Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on. */ - idfa?: string + groups?: { + [k: string]: unknown + } /** - * Identifier for Vendor. _(iOS)_ + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - idfv?: string + includeRawUserAgent?: boolean /** - * Google Play Services advertising ID. _(Android)_ + * The language set by the user. */ - adid?: string + language?: string /** - * Android ID (not the advertising ID). _(Android)_ + * The name of the library that generated the event. */ - android_id?: string + library?: string /** - * An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously. + * The name of the mobile operating system or browser that the user is using. */ - event_id?: number + os_name?: string /** - * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. + * The version of the mobile operating system or browser the user is using. */ - insert_id?: string + os_version?: string /** - * The name of the library that generated the event. + * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. */ - library?: string + platform?: string /** - * The list of products purchased. + * The user agent of the device sending the event. */ - products?: { - /** - * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. - */ - price?: number - /** - * The quantity of the item purchased. Defaults to 1 if not specified. - */ - quantity?: number - /** - * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. - */ - revenue?: number - /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. - */ - productId?: string - /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. - */ - revenueType?: string + userAgent?: string + /** + * The user agent data of device sending the event + */ + userAgentData?: { + model?: string + platformVersion?: string + } + /** + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + */ + userAgentParsing?: boolean + /** + * An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + */ + user_properties?: { [k: string]: unknown - }[] + } /** * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ @@ -200,44 +211,19 @@ export interface Payload { [k: string]: unknown } /** - * UTM Tracking Properties - */ - utm_properties?: { - utm_source?: string - utm_medium?: string - utm_campaign?: string - utm_term?: string - utm_content?: string - } - /** - * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” - */ - referrer?: string - /** - * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). - */ - use_batch_endpoint?: boolean - /** - * The user agent of the device sending the event. - */ - userAgent?: string - /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. */ - userAgentParsing?: boolean + device_id?: string /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. */ - includeRawUserAgent?: boolean + insert_id?: string /** * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null /** - * The user agent data of device sending the event + * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ - userAgentData?: { - model?: string - platformVersion?: string - } + time?: string | number } diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index 06b791a6319..168ab189f69 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -6,7 +6,7 @@ import { common_fields } from '../fields/common-fields' import { common_track_fields } from '../fields/common-track-fields' import { common_track_identify_fields } from '../fields/common-track-identify-fields' import { autocapture_fields } from '../fields/autocapture-fields' -import { min_id_length } from '../fields/misc-fields' +import { min_id_length, device_id, time, insert_id, utm_properties, referrer } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Log Event', @@ -17,7 +17,12 @@ const action: ActionDefinition = { ...common_track_fields, ...common_track_identify_fields, ...autocapture_fields, - min_id_length + device_id, + insert_id, + min_id_length, + time, + utm_properties, + referrer }, perform: (request, { payload, settings }) => { return send(request, payload, settings, false) diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index e0e691067d2..2b8af9bb347 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts @@ -6,91 +6,118 @@ export interface Payload { */ user_id?: string | null /** - * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. + * When enabled, track revenue with each product within the event. When disabled, track total revenue once for the event. */ - device_id?: string + trackRevenuePerProduct?: boolean /** - * A unique identifier for your event. - */ - event_type: string - /** - * The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source. - */ - session_id?: string | number - /** - * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. - */ - time?: string | number - /** - * An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + * The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. */ - event_properties?: { + setOnce?: { + /** + * The referrer of the web request. + */ + initial_referrer?: string + initial_utm_source?: string + initial_utm_medium?: string + initial_utm_campaign?: string + initial_utm_term?: string + initial_utm_content?: string [k: string]: unknown } /** - * An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + * The following fields will be set as user properties for every event. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. */ - user_properties?: { + setAlways?: { + referrer?: string + utm_source?: string + utm_medium?: string + utm_campaign?: string + utm_term?: string + utm_content?: string [k: string]: unknown } /** - * Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on. + * Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0. */ - groups?: { + add?: { [k: string]: unknown } /** - * The current version of your application. + * Google Play Services advertising ID. _(Android)_ */ - app_version?: string + adid?: string /** - * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. + * Android ID (not the advertising ID). _(Android)_ */ - platform?: string + android_id?: string /** - * The name of the mobile operating system or browser that the user is using. + * An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously. */ - os_name?: string + event_id?: number /** - * The version of the mobile operating system or browser the user is using. + * An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. */ - os_version?: string + event_properties?: { + [k: string]: unknown + } /** - * The device brand that the user is using. + * A unique identifier for your event. */ - device_brand?: string + event_type: string /** - * The device manufacturer that the user is using. + * Identifier for Advertiser. _(iOS)_ */ - device_manufacturer?: string + idfa?: string /** - * The device model that the user is using. + * Identifier for Vendor. _(iOS)_ */ - device_model?: string + idfv?: string /** - * The carrier that the user is using. + * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. */ - carrier?: string + ip?: string /** - * The current country of the user. + * The current Latitude of the user. */ - country?: string + location_lat?: number /** - * The current city of the user. + * The current Longitude of the user. */ - city?: string + location_lng?: number /** - * The current Designated Market Area of the user. + * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. */ - dma?: string + price?: number /** - * The language set by the user. + * An identifier for the item purchased. You must send a price and quantity or revenue with this field. */ - language?: string + productId?: string /** - * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. + * The list of products purchased. */ - price?: number + products?: { + /** + * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. + */ + price?: number + /** + * The quantity of the item purchased. Defaults to 1 if not specified. + */ + quantity?: number + /** + * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. + */ + revenue?: number + /** + * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + */ + productId?: string + /** + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + */ + revenueType?: string + [k: string]: unknown + }[] /** * The quantity of the item purchased. Defaults to 1 if not specified. */ @@ -99,111 +126,103 @@ export interface Payload { * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. */ revenue?: number - /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. - */ - productId?: string /** * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. */ revenueType?: string /** - * The current Latitude of the user. + * The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source. */ - location_lat?: number + session_id?: string | number /** - * The current Longitude of the user. + * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ - location_lng?: number + use_batch_endpoint?: boolean /** - * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. + * The current version of your application. */ - ip?: string + app_version?: string /** - * Identifier for Advertiser. _(iOS)_ + * The carrier that the user is using. */ - idfa?: string + carrier?: string /** - * Identifier for Vendor. _(iOS)_ + * The current city of the user. */ - idfv?: string + city?: string /** - * Google Play Services advertising ID. _(Android)_ + * The current country of the user. */ - adid?: string + country?: string /** - * Android ID (not the advertising ID). _(Android)_ + * The current region of the user. */ - android_id?: string + region?: string /** - * An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously. + * The device brand that the user is using. */ - event_id?: number + device_brand?: string /** - * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. + * The device manufacturer that the user is using. */ - insert_id?: string + device_manufacturer?: string /** - * The name of the library that generated the event. + * The device model that the user is using. */ - library?: string + device_model?: string /** - * The list of products purchased. + * The current Designated Market Area of the user. */ - products?: { - /** - * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. - */ - price?: number - /** - * The quantity of the item purchased. Defaults to 1 if not specified. - */ - quantity?: number - /** - * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. - */ - revenue?: number - /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. - */ - productId?: string - /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. - */ - revenueType?: string - [k: string]: unknown - }[] + dma?: string /** - * The following fields will only be set as user properties if they do not already have a value. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. + * Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on. */ - setOnce?: { - /** - * The referrer of the web request. - */ - initial_referrer?: string - initial_utm_source?: string - initial_utm_medium?: string - initial_utm_campaign?: string - initial_utm_term?: string - initial_utm_content?: string + groups?: { [k: string]: unknown } /** - * The following fields will be set as user properties for every event. If 'Autocapture Attribution' is enabled, UTM and attribution values in this field will be ignored. + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - setAlways?: { - referrer?: string - utm_source?: string - utm_medium?: string - utm_campaign?: string - utm_term?: string - utm_content?: string - [k: string]: unknown + includeRawUserAgent?: boolean + /** + * The language set by the user. + */ + language?: string + /** + * The name of the library that generated the event. + */ + library?: string + /** + * The name of the mobile operating system or browser that the user is using. + */ + os_name?: string + /** + * The version of the mobile operating system or browser the user is using. + */ + os_version?: string + /** + * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. + */ + platform?: string + /** + * The user agent of the device sending the event. + */ + userAgent?: string + /** + * The user agent data of device sending the event + */ + userAgentData?: { + model?: string + platformVersion?: string } /** - * Increment a user property by a number with add. If the user property doesn't have a value set yet, it's initialized to 0. + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field */ - add?: { + userAgentParsing?: boolean + /** + * An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + */ + user_properties?: { [k: string]: unknown } /** @@ -229,30 +248,19 @@ export interface Payload { [k: string]: unknown } /** - * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). - */ - use_batch_endpoint?: boolean - /** - * The user agent of the device sending the event. - */ - userAgent?: string - /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. */ - userAgentParsing?: boolean + device_id?: string /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. */ - includeRawUserAgent?: boolean + insert_id?: string /** * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null /** - * The user agent data of device sending the event + * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ - userAgentData?: { - model?: string - platformVersion?: string - } + time?: string | number } diff --git a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts index 487504d4a27..5e0952f47a0 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -7,7 +7,7 @@ import { common_fields } from '../fields/common-fields' import { common_track_fields } from '../fields/common-track-fields' import { common_track_identify_fields } from '../fields/common-track-identify-fields' import { trackRevenuePerProduct, setOnce, setAlways, add } from './fields' -import { min_id_length } from '../fields/misc-fields' +import { min_id_length , device_id, time, insert_id } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Log Event V2', @@ -22,7 +22,10 @@ const action: ActionDefinition = { ...common_track_fields, ...common_track_identify_fields, ...autocapture_fields, - min_id_length + device_id, + insert_id, + min_id_length, + time }, perform: (request, { payload, settings }) => { return send(request, payload, settings, false) diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts index 85d52626920..daf897c77df 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts @@ -10,21 +10,17 @@ export interface Payload { */ user_id?: string | null /** - * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. - */ - device_id?: string - /** - * A unique identifier for your event. + * Google Play Services advertising ID. _(Android)_ */ - event_type: string + adid?: string /** - * The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source. + * Android ID (not the advertising ID). _(Android)_ */ - session_id?: string | number + android_id?: string /** - * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. + * An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously. */ - time?: string | number + event_id?: number /** * An object of key-value pairs that represent additional data to be sent along with the event. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. */ @@ -32,155 +28,170 @@ export interface Payload { [k: string]: unknown } /** - * An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + * A unique identifier for your event. */ - user_properties?: { - [k: string]: unknown - } + event_type: string /** - * Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on. + * Identifier for Advertiser. _(iOS)_ */ - groups?: { - [k: string]: unknown - } + idfa?: string /** - * The current version of your application. + * Identifier for Vendor. _(iOS)_ */ - app_version?: string + idfv?: string /** - * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. + * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. */ - platform?: string + ip?: string /** - * The name of the mobile operating system or browser that the user is using. + * The current Latitude of the user. */ - os_name?: string + location_lat?: number /** - * The version of the mobile operating system or browser the user is using. + * The current Longitude of the user. */ - os_version?: string + location_lng?: number /** - * The device brand that the user is using. + * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. */ - device_brand?: string + price?: number /** - * The device manufacturer that the user is using. + * An identifier for the item purchased. You must send a price and quantity or revenue with this field. */ - device_manufacturer?: string + productId?: string /** - * The device model that the user is using. + * The list of products purchased. */ - device_model?: string + products?: { + /** + * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. + */ + price?: number + /** + * The quantity of the item purchased. Defaults to 1 if not specified. + */ + quantity?: number + /** + * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. + */ + revenue?: number + /** + * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + */ + productId?: string + /** + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + */ + revenueType?: string + [k: string]: unknown + }[] /** - * The carrier that the user is using. + * The quantity of the item purchased. Defaults to 1 if not specified. */ - carrier?: string + quantity?: number /** - * The current country of the user. + * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. */ - country?: string + revenue?: number /** - * The current region of the user. + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. */ - region?: string + revenueType?: string /** - * The current city of the user. + * The start time of the session, necessary if you want to associate events with a particular system. To use automatic Amplitude session tracking in browsers, enable Analytics 2.0 on your connected source. */ - city?: string + session_id?: string | number /** - * The current Designated Market Area of the user. + * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). */ - dma?: string + use_batch_endpoint?: boolean /** - * The language set by the user. + * The current version of your application. */ - language?: string + app_version?: string /** - * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. + * The carrier that the user is using. */ - price?: number + carrier?: string /** - * The quantity of the item purchased. Defaults to 1 if not specified. + * The current city of the user. */ - quantity?: number + city?: string /** - * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. + * The current country of the user. */ - revenue?: number + country?: string /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + * The current region of the user. */ - productId?: string + region?: string /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + * The device brand that the user is using. */ - revenueType?: string + device_brand?: string /** - * The current Latitude of the user. + * The device manufacturer that the user is using. */ - location_lat?: number + device_manufacturer?: string /** - * The current Longitude of the user. + * The device model that the user is using. */ - location_lng?: number + device_model?: string /** - * The IP address of the user. Use "$remote" to use the IP address on the upload request. Amplitude will use the IP address to reverse lookup a user's location (city, country, region, and DMA). Amplitude has the ability to drop the location and IP address from events once it reaches our servers. + * The current Designated Market Area of the user. */ - ip?: string + dma?: string /** - * Identifier for Advertiser. _(iOS)_ + * Groups of users for the event as an event-level group. You can only track up to 5 groups. **Note:** This Amplitude feature is only available to Enterprise customers who have purchased the Accounts add-on. */ - idfa?: string + groups?: { + [k: string]: unknown + } /** - * Identifier for Vendor. _(iOS)_ + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - idfv?: string + includeRawUserAgent?: boolean /** - * Google Play Services advertising ID. _(Android)_ + * The language set by the user. */ - adid?: string + language?: string /** - * Android ID (not the advertising ID). _(Android)_ + * The name of the library that generated the event. */ - android_id?: string + library?: string /** - * An incrementing counter to distinguish events with the same user ID and timestamp from each other. Amplitude recommends you send an event ID, increasing over time, especially if you expect events to occur simultanenously. + * The name of the mobile operating system or browser that the user is using. */ - event_id?: number + os_name?: string /** - * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. + * The version of the mobile operating system or browser the user is using. */ - insert_id?: string + os_version?: string /** - * The name of the library that generated the event. + * Platform of the device. If using analytics.js to send events from a Browser and no if no Platform value is provided, the value "Web" will be sent. */ - library?: string + platform?: string /** - * The list of products purchased. + * The user agent of the device sending the event. */ - products?: { - /** - * The price of the item purchased. Required for revenue data if the revenue field is not sent. You can use negative values to indicate refunds. - */ - price?: number - /** - * The quantity of the item purchased. Defaults to 1 if not specified. - */ - quantity?: number - /** - * Revenue = price * quantity. If you send all 3 fields of price, quantity, and revenue, then (price * quantity) will be used as the revenue value. You can use negative values to indicate refunds. - */ - revenue?: number - /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. - */ - productId?: string - /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. - */ - revenueType?: string + userAgent?: string + /** + * The user agent data of device sending the event + */ + userAgentData?: { + model?: string + platformVersion?: string + } + /** + * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + */ + userAgentParsing?: boolean + /** + * An object of key-value pairs that represent additional data tied to the user. You can store property values in an array, but note that Amplitude only supports one-dimensional arrays. Date values are transformed into string values. Object depth may not exceed 40 layers. + */ + user_properties?: { [k: string]: unknown - }[] + } /** * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ @@ -204,44 +215,19 @@ export interface Payload { [k: string]: unknown } /** - * UTM Tracking Properties - */ - utm_properties?: { - utm_source?: string - utm_medium?: string - utm_campaign?: string - utm_term?: string - utm_content?: string - } - /** - * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” - */ - referrer?: string - /** - * If true, events are sent to Amplitude's `batch` endpoint rather than their `httpapi` events endpoint. Enabling this setting may help reduce 429s – or throttling errors – from Amplitude. More information about Amplitude's throttling is available in [their docs](https://developers.amplitude.com/docs/batch-event-upload-api#429s-in-depth). - */ - use_batch_endpoint?: boolean - /** - * The user agent of the device sending the event. + * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. */ - userAgent?: string + device_id?: string /** - * Enabling this setting will set the Device manufacturer, Device Model and OS Name properties based on the user agent string provided in the userAgent field + * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. */ - userAgentParsing?: boolean + insert_id?: string /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ - includeRawUserAgent?: boolean + time?: string | number /** * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null - /** - * The user agent data of device sending the event - */ - userAgentData?: { - model?: string - platformVersion?: string - } } diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts index 15ed90bc02f..44de5f29be8 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -7,7 +7,7 @@ import { common_track_fields } from '../fields/common-track-fields' import { common_track_identify_fields } from '../fields/common-track-identify-fields' import { autocapture_fields } from '../fields/autocapture-fields' import { trackRevenuePerProduct } from './fields' -import { min_id_length } from '../fields/misc-fields' +import { min_id_length, time, device_id, insert_id, utm_properties, referrer} from '../fields/misc-fields' const action: ActionDefinition = { title: 'Log Purchase', @@ -19,7 +19,12 @@ const action: ActionDefinition = { ...common_track_fields, ...common_track_identify_fields, ...autocapture_fields, - min_id_length + device_id, + insert_id, + time, + min_id_length, + utm_properties, + referrer }, perform: (request, { payload, settings }) => { return send(request, payload, settings, true) diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index 0d0be7a5eb6..60a54f91bc1 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -11,7 +11,12 @@ const action: ActionDefinition = { description: 'Merge two users together that would otherwise have different User IDs tracked in Amplitude.', defaultSubscription: 'type = "alias"', fields: { - user_id, + user_id: { + ...user_id, + default: { + '@path': '$.previousId' + } + }, global_user_id, min_id_length }, From a4b5cd3873368fa1968a6af570a705cb17ec6831 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 12:45:11 +0000 Subject: [PATCH 37/38] fix spelling --- .../src/destinations/amplitude/events-constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/amplitude/events-constants.ts b/packages/destination-actions/src/destinations/amplitude/events-constants.ts index 30357aeb766..6cdd59b67a1 100644 --- a/packages/destination-actions/src/destinations/amplitude/events-constants.ts +++ b/packages/destination-actions/src/destinations/amplitude/events-constants.ts @@ -1,4 +1,4 @@ -const REVENUE_KETS = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] +const REVENUE_KEYS = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] const USER_PROPERTY_KEYS = ['utm_properties', 'referrer', 'setOnce', 'setAlways','add','autocaptureAttributionEnabled','autocaptureAttributionSet','autocaptureAttributionSetOnce','autocaptureAttributionUnset'] -export const KEYS_TO_OMIT = [...REVENUE_KETS, ...USER_PROPERTY_KEYS, 'trackRevenuePerProduct'] +export const KEYS_TO_OMIT = [...REVENUE_KEYS, ...USER_PROPERTY_KEYS, 'trackRevenuePerProduct'] export const DESTINATION_INTEGRATION_NAME = 'Actions Amplitude' \ No newline at end of file From ce18fd3f03409c3bcee86763084329ac17b3d7ee Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 1 Dec 2025 12:53:13 +0000 Subject: [PATCH 38/38] missing field --- .../amplitude/groupIdentifyUser/generated-types.ts | 4 ++++ .../amplitude/groupIdentifyUser/index.ts | 3 ++- .../amplitude/logEvent/generated-types.ts | 14 ++++++++++++++ .../amplitude/logPurchase/generated-types.ts | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts index a326b669d5c..2ae1718a353 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts @@ -13,6 +13,10 @@ export interface Payload { * Amplitude will deduplicate subsequent events sent with this ID we have already seen before within the past 7 days. Amplitude recommends generating a UUID or using some combination of device ID, user ID, event type, event ID, and time. */ insert_id?: string + /** + * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. + */ + min_id_length?: number | null /** * A device-specific identifier, such as the Identifier for Vendor on iOS. Required unless user ID is present. If a device ID is not sent with the event, it will be set to a hashed version of the user ID. */ diff --git a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts index 8f3d89acc3d..da58a419ab7 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -5,7 +5,7 @@ import dayjs from '../../../lib/dayjs' import { getEndpointByRegion } from '../common-functions' import { common_fields } from '../fields/common-fields' import { group_properties, group_type, group_value } from './fields' -import { device_id, time, insert_id } from '../fields/misc-fields' +import { device_id, time, insert_id, min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Group Identify User', @@ -20,6 +20,7 @@ const action: ActionDefinition = { }, time, insert_id, + min_id_length, device_id, group_properties, group_type, diff --git a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts index 49cc33c52a1..be079a15fc9 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts @@ -226,4 +226,18 @@ export interface Payload { * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ time?: string | number + /** + * UTM Tracking Properties + */ + utm_properties?: { + utm_source?: string + utm_medium?: string + utm_campaign?: string + utm_term?: string + utm_content?: string + } + /** + * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” + */ + referrer?: string } diff --git a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts index daf897c77df..7ed9fd1e371 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts @@ -230,4 +230,18 @@ export interface Payload { * Amplitude has a default minimum id length of 5 characters for user_id and device_id fields. This field allows the minimum to be overridden to allow shorter id lengths. */ min_id_length?: number | null + /** + * UTM Tracking Properties + */ + utm_properties?: { + utm_source?: string + utm_medium?: string + utm_campaign?: string + utm_term?: string + utm_content?: string + } + /** + * The referrer of the web request. Sent to Amplitude as both last touch “referrer” and first touch “initial_referrer” + */ + referrer?: string }