diff --git a/packages/actions-shared/src/amplitude/constants.ts b/packages/actions-shared/src/amplitude/constants.ts new file mode 100644 index 00000000000..4b53ee47f84 --- /dev/null +++ b/packages/actions-shared/src/amplitude/constants.ts @@ -0,0 +1,23 @@ +export const AMPLITUDE_ATTRIBUTION_KEYS = [ + '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', + 'ttclid', + 'twclid' +] as const + +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..61fe58c3b65 --- /dev/null +++ b/packages/actions-shared/src/amplitude/types.ts @@ -0,0 +1,11 @@ +import { AMPLITUDE_ATTRIBUTION_KEYS } from './constants' + +export type AmplitudeAttributionKey = typeof AMPLITUDE_ATTRIBUTION_KEYS[number] + +export type AmplitudeSetOnceAttributionKey = `initial_${AmplitudeAttributionKey}` + +export type AmplitudeAttributionValues = Record + +export type AmplitudeAttributionUnsetValues = Record + +export type AmplitudeSetOnceAttributionValues = Record \ 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/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/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..ecc82adeabd --- /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: 'sessionId', + 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 8079657ed66..dece724f2aa 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 } 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 { Analytics } from '@segment/analytics-next' - -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,56 +57,39 @@ 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 + }, + 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 { enableAutocaptureAttribution } = payload + const isNewSession = enrichWithSessionId(context, payload, analytics) + if(enableAutocaptureAttribution){ + enrichWithAutocaptureAttribution(context, payload, analytics, isNewSession) } - - 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)) { - 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['Actions Amplitude']) { - context.updateEvent('integrations.Actions Amplitude', {}) - context.updateEvent('integrations.Actions Amplitude.session_id', id) - } - - return } } -export default action +export default action \ No newline at end of file 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..565d2357a29 --- /dev/null +++ b/packages/browser-destinations/destinations/amplitude-plugins/src/sessionId/sessionid-functions.ts @@ -0,0 +1,99 @@ +/* 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) + } + 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 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__/amplitude.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/amplitude.test.ts index 374c9490bd0..5c558a2cdda 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, } `) }) @@ -1087,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", @@ -1101,7 +1091,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1155,7 +1144,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1209,7 +1197,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1304,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", @@ -1318,7 +1303,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1591,9 +1575,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 +1587,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1659,7 +1640,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1713,7 +1693,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1905,9 +1884,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 +1896,6 @@ describe('Amplitude', () => { "user_properties": Object {}, }, ], - "options": undefined, } `) }) @@ -1946,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', () => { @@ -1984,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"` ) }) @@ -2017,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"` ) }) @@ -2038,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"` ) }) @@ -2071,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"` ) }) @@ -2104,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"` ) }) @@ -2125,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"` ) }) @@ -2146,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"` ) }) @@ -2175,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"` ) }) @@ -2212,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__/autocapture-attribution.test.ts b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts new file mode 100644 index 00000000000..90c0f6cbe48 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/__tests__/autocapture-attribution.test.ts @@ -0,0 +1,399 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Amplitude from '../index' +import {AmplitudeAttributionValues, AmplitudeSetOnceAttributionValues, AmplitudeAttributionUnsetValues} from '@segment/actions-shared' + +const testDestination = createTestIntegration(Amplitude) +const timestamp = '2021-08-17T15:21:15.449Z' + +describe('Amplitude', () => { + describe('logEvent V2', () => { + 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: '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: '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: 'EMPTY', + initial_ttclid: 'EMPTY', + initial_twclid: 'EMPTY', + initial_utm_id: 'EMPTY' + } + + 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: Partial = { + 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', + 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 + } + }, + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + enabled: true, + set_once, + set, + unset + } + } + }, + context: { + + page: { + referrer: 'referrer-from-page-context' // should get dropped + }, + campaign: { + 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, + 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 + 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: { + first_name: "Billybob", // carried over from the setOnce mapping + initial_dclid: "EMPTY", + initial_fbclid: "EMPTY", + initial_gbraid: "EMPTY", + initial_gclid: "initial-gclid-from-integrations-object", + 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: "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: "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: "EMPTY", + }, + $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, + }) + }) + + 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 + } + }, + integrations: { + 'Actions Amplitude': { + autocapture_attribution: { + enabled: true, + set_once: {}, // no attribution values provided, however we'll ignore the mappged values as enabled is true + set: {}, + unset: {} + } + } + }, + context: { + 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 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({ + 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 + } + }, + 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 + }, + 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/__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__/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/__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/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/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/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/events-constants.ts b/packages/destination-actions/src/destinations/amplitude/events-constants.ts new file mode 100644 index 00000000000..6cdd59b67a1 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/events-constants.ts @@ -0,0 +1,4 @@ +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_KEYS, ...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/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/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/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/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/generated-types.ts index 05340838d5a..2ae1718a353 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,21 @@ 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. + * 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. + * 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. */ - time?: string + device_id?: string /** * Additional data tied to the group in Amplitude. */ @@ -31,8 +35,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 4e6bea16693..da58a419ab7 100644 --- a/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/groupIdentifyUser/index.ts @@ -2,7 +2,10 @@ 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 { common_fields } from '../fields/common-fields' +import { group_properties, group_type, group_value } from './fields' +import { device_id, time, insert_id, min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Group Identify User', @@ -10,71 +13,18 @@ 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: { - 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' - } + ...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: { - 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' - } - }, - 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 - }, - 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' - } + time, + insert_id, + min_id_length, + device_id, + group_properties, + group_type, + 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/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/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/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..9e6d55558a2 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/generated-types.ts @@ -6,15 +6,41 @@ export interface Payload { */ user_id?: string | null /** - * A device specific identifier, such as the Identifier for Vendor (IDFV) on iOS. Required unless user ID is present. + * The current version of your application. */ - device_id?: string + app_version?: 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 carrier that the user is using. */ - user_properties?: { - [k: string]: unknown - } + carrier?: string + /** + * The current city of the user. + */ + city?: string + /** + * The current country of the user. + */ + country?: string + /** + * The current region of the user. + */ + region?: string + /** + * The device brand that the user is using. + */ + 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. */ @@ -22,81 +48,96 @@ export interface Payload { [k: string]: unknown } /** - * Version of the app the user is on. + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - app_version?: string + includeRawUserAgent?: boolean /** - * The platform of the user's device. + * The language set by the user. */ - platform?: string + language?: string + /** + * The name of the library that generated the event. + */ + library?: 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. + * 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_brand?: string - /** - * The manufacturer of the user's device. - */ - device_manufacturer?: string + platform?: string /** - * The model of the user's device. + * The user agent of the device sending the event. */ - device_model?: string + userAgent?: string /** - * The user's mobile carrier. + * The user agent data of device sending the event */ - carrier?: string + userAgentData?: { + model?: string + platformVersion?: string + } /** - * The country in which the user is located. + * 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 geographical region in which the user is located. + * 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 city in which the user is located. + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - city?: string + autocaptureAttributionEnabled?: boolean /** - * The Designated Market Area in which the user is located. + * Utility field used to detect if any attribution values need to be set. */ - dma?: string + autocaptureAttributionSet?: { + [k: string]: unknown + } /** - * Language the user has set on their device or browser. + * 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 */ @@ -107,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 ca38dca62e4..239a204ae93 100644 --- a/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/identifyUser/index.ts @@ -1,13 +1,12 @@ -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 { 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, device_id, insert_id, referrer, utm_properties } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Identify User', @@ -15,298 +14,35 @@ 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: { + ...common_fields, + ...common_track_identify_fields, + ...autocapture_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. Required unless device ID is present.', - default: { - '@path': '$.userId' - } + ...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: { - 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.' }, + insert_id, 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' - } + ...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: { - 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' - } - }, - 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.' - }, - 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' - } - }, - userAgentData + ...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." + }, + min_id_length, + paying, + start_version, + referrer, + utm_properties }, - 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..a3a3251cae0 100644 --- a/packages/destination-actions/src/destinations/amplitude/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/index.ts @@ -6,66 +6,9 @@ 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' -/** 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/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/generated-types.ts index dd60578bfa2..be079a15fc9 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,49 +24,95 @@ 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 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. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. + */ + revenue?: number + /** + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + */ + revenueType?: 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 + /** + * 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 current version of your application. + */ + app_version?: string /** * The carrier that the user is using. */ carrier?: string + /** + * The current city of the user. + */ + city?: string /** * The current country of the user. */ @@ -80,119 +122,110 @@ export interface Payload { */ region?: string /** - * The current city of the user. + * The device brand that the user is using. */ - city?: string + device_brand?: string /** - * The current Designated Market Area of the user. + * The device manufacturer that the user is using. */ - dma?: string + device_manufacturer?: string /** - * The language set by the user. + * The device model that the user is using. */ - language?: string + device_model?: 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 current Designated Market Area of the user. */ - price?: number + dma?: string /** - * The quantity of the item purchased. Defaults to 1 if not specified. + * 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. */ - quantity?: number + groups?: { + [k: string]: unknown + } /** - * 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. + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - revenue?: number + includeRawUserAgent?: boolean /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + * The language set by the user. */ - productId?: string + language?: string /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + * The name of the library that generated the event. */ - revenueType?: string + library?: string /** - * The current Latitude of the user. + * The name of the mobile operating system or browser that the user is using. */ - location_lat?: number + os_name?: string /** - * The current Longitude of the user. + * The version of the mobile operating system or browser the user is using. */ - location_lng?: number + os_version?: 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. You can submit a request to Amplitude's platform specialist team here to configure this for you. + * 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. */ - ip?: string + platform?: string /** - * Identifier for Advertiser. _(iOS)_ + * The user agent of the device sending the event. */ - idfa?: string + userAgent?: string /** - * Identifier for Vendor. _(iOS)_ + * The user agent data of device sending the event */ - idfv?: string + userAgentData?: { + model?: string + platformVersion?: string + } /** - * Google Play Services advertising ID. _(Android)_ + * 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 */ - adid?: string + userAgentParsing?: boolean /** - * Android ID (not the advertising ID). _(Android)_ + * 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. */ - android_id?: string + user_properties?: { + [k: string]: unknown + } /** - * 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. + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - event_id?: number + autocaptureAttributionEnabled?: boolean /** - * 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. + * Utility field used to detect if any attribution values need to be set. */ - insert_id?: string + autocaptureAttributionSet?: { + [k: string]: unknown + } /** - * The name of the library that generated the event. + * Utility field used to detect if any attribution values need to be set_once. */ - library?: string + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } /** - * The list of products purchased. + * Utility field used to detect if any attribution values need to be unset. */ - 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 + 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). + * 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. */ - use_batch_endpoint?: boolean + device_id?: string /** - * The user agent of the device sending the event. + * 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?: string + insert_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 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. */ - userAgentParsing?: boolean + min_id_length?: number | null /** - * 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 /** * UTM Tracking Properties */ @@ -207,15 +240,4 @@ export interface Payload { * 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 lenght 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/logEvent/index.ts b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts index aa10e91d9d6..168ab189f69 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEvent/index.ts @@ -1,242 +1,32 @@ -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 { send } from '../events-functions' +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, device_id, time, insert_id, utm_properties, referrer } from '../fields/misc-fields' -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.', 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' - } - } - ] - } - }, - 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 - }, - 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 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' - }, - userAgentData + ...common_fields, + ...common_track_fields, + ...common_track_identify_fields, + ...autocapture_fields, + device_id, + insert_id, + min_id_length, + time, + utm_properties, + referrer }, 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) } } -export default action +export default action \ No newline at end of file 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/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logEventV2/generated-types.ts index 787b698616a..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,95 +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. + * 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_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. - */ - 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. - */ - app_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. + * Google Play Services advertising ID. _(Android)_ */ - platform?: string + adid?: string /** - * The name of the mobile operating system or browser that the user is using. + * Android ID (not the advertising ID). _(Android)_ */ - os_name?: string + android_id?: string /** - * The version of the mobile operating system or browser 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_version?: string + event_id?: number /** - * The device brand that 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. */ - device_brand?: string + event_properties?: { + [k: string]: unknown + } /** - * The device manufacturer that the user is using. + * A unique identifier for your event. */ - device_manufacturer?: string + event_type: string /** - * The device model that the user is using. + * Identifier for Advertiser. _(iOS)_ */ - device_model?: string + idfa?: string /** - * The carrier that the user is using. + * Identifier for Vendor. _(iOS)_ */ - carrier?: string + idfv?: string /** - * The current country of the user. + * 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. */ - country?: string + ip?: string /** - * The current region of the user. + * The current Latitude of the user. */ - region?: 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. */ @@ -103,138 +126,141 @@ 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. You can submit a request to Amplitude's platform specialist team here to configure this for you. + * 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 device model that the user is using. + */ + device_model?: string + /** + * The current Designated Market Area of the user. + */ + dma?: 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. + */ + groups?: { + [k: string]: unknown + } + /** + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + */ + includeRawUserAgent?: boolean + /** + * The language set by the user. + */ + language?: string /** * The name of the library that generated the event. */ library?: string /** - * The list of products purchased. + * The name of the mobile operating system or browser that the user is using. */ - 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 - }[] + os_name?: string /** - * The following fields will only be set as user properties if they do not already have a value. + * The version of the mobile operating system or browser the user is using. */ - 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 + 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 } /** - * The following fields will be set as user properties for every event. + * 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 */ - setAlways?: { - referrer?: string - utm_source?: string - utm_medium?: string - utm_campaign?: string - utm_term?: string - utm_content?: string + 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 } /** - * 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. + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - add?: { + autocaptureAttributionEnabled?: boolean + /** + * Utility field used to detect if any attribution values need to be set. + */ + autocaptureAttributionSet?: { [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 any attribution values need to be set_once. */ - use_batch_endpoint?: boolean + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } /** - * The user agent of the device sending the event. + * Utility field used to detect if any attribution values need to be unset. */ - userAgent?: string + autocaptureAttributionUnset?: { + [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. + * 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 ef20b455231..5e0952f47a0 100644 --- a/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logEventV2/index.ts @@ -1,290 +1,34 @@ -import { ActionDefinition, omit, removeUndefined } from '@segment/actions-core' -import dayjs from 'dayjs' -import compact from '../compact' -import { eventSchema } from '../event-schema' +import { ActionDefinition} from '@segment/actions-core' 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' - -export interface AmplitudeEvent extends Omit { - library?: string - time?: number - session_id?: number - options?: { - min_id_length: number - } -} - -const revenueKeys = ['revenue', 'price', 'productId', 'quantity', 'revenueType'] +import { autocapture_fields } from '../fields/autocapture-fields' +import { send } from '../events-functions' +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 , device_id, time, insert_id } from '../fields/misc-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' - } - } - ] - } - }, - setOnce: { - label: 'Set Once', - description: 'The following fields will only be set as user properties if they do not already have a value.', - 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.', - 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: { - 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' - }, - userAgentData + ...common_fields, + trackRevenuePerProduct, + setOnce, + setAlways, + add, + ...common_track_fields, + ...common_track_identify_fields, + ...autocapture_fields, + device_id, + insert_id, + min_id_length, + time }, perform: (request, { payload, settings }) => { - const { - time, - session_id, - userAgent, - userAgentParsing, - includeRawUserAgent, - userAgentData, - min_id_length, - library, - setOnce, - setAlways, - add, - ...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 (min_id_length && min_id_length > 0) { - options = { min_id_length } - } - - const setUserProperties = ( - name: '$setOnce' | '$set' | '$add', - obj: Payload['setOnce'] | Payload['setAlways'] | Payload['add'] - ) => { - if (compact(obj)) { - properties.user_properties = { ...properties.user_properties, [name]: obj } - } - } - - setUserProperties('$setOnce', setOnce) - setUserProperties('$set', setAlways) - setUserProperties('$add', add) - - 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/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/generated-types.ts b/packages/destination-actions/src/destinations/amplitude/logPurchase/generated-types.ts index acc0a64d7c5..7ed9fd1e371 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,49 +28,95 @@ 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 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. **Note:** You will need to explicitly set this if you are using the Amplitude in cloud-mode. + */ + revenue?: number + /** + * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + */ + revenueType?: 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 + /** + * 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 current version of your application. + */ + app_version?: string /** * The carrier that the user is using. */ carrier?: string + /** + * The current city of the user. + */ + city?: string /** * The current country of the user. */ @@ -84,119 +126,110 @@ export interface Payload { */ region?: string /** - * The current city of the user. + * The device brand that the user is using. */ - city?: string + device_brand?: string /** - * The current Designated Market Area of the user. + * The device manufacturer that the user is using. */ - dma?: string + device_manufacturer?: string /** - * The language set by the user. + * The device model that the user is using. */ - language?: string + device_model?: 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 current Designated Market Area of the user. */ - price?: number + dma?: string /** - * The quantity of the item purchased. Defaults to 1 if not specified. + * 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. */ - quantity?: number + groups?: { + [k: string]: unknown + } /** - * 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. + * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field */ - revenue?: number + includeRawUserAgent?: boolean /** - * An identifier for the item purchased. You must send a price and quantity or revenue with this field. + * The language set by the user. */ - productId?: string + language?: string /** - * The type of revenue for the item purchased. You must send a price and quantity or revenue with this field. + * The name of the library that generated the event. */ - revenueType?: string + library?: string /** - * The current Latitude of the user. + * The name of the mobile operating system or browser that the user is using. */ - location_lat?: number + os_name?: string /** - * The current Longitude of the user. + * The version of the mobile operating system or browser the user is using. */ - location_lng?: number + os_version?: 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. You can submit a request to Amplitude's platform specialist team here to configure this for you. + * 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. */ - ip?: string + platform?: string /** - * Identifier for Advertiser. _(iOS)_ + * The user agent of the device sending the event. */ - idfa?: string + userAgent?: string /** - * Identifier for Vendor. _(iOS)_ + * The user agent data of device sending the event */ - idfv?: string + userAgentData?: { + model?: string + platformVersion?: string + } /** - * Google Play Services advertising ID. _(Android)_ + * 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 */ - adid?: string + userAgentParsing?: boolean /** - * Android ID (not the advertising ID). _(Android)_ + * 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. */ - android_id?: string + user_properties?: { + [k: string]: unknown + } /** - * 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. + * Utility field used to detect if Autocapture Attribution Plugin is enabled. */ - event_id?: number + autocaptureAttributionEnabled?: boolean /** - * 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. + * Utility field used to detect if any attribution values need to be set. */ - insert_id?: string + autocaptureAttributionSet?: { + [k: string]: unknown + } /** - * The name of the library that generated the event. + * Utility field used to detect if any attribution values need to be set_once. */ - library?: string + autocaptureAttributionSetOnce?: { + [k: string]: unknown + } /** - * The list of products purchased. + * Utility field used to detect if any attribution values need to be unset. */ - 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 + 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). + * 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. */ - use_batch_endpoint?: boolean + device_id?: string /** - * The user agent of the device sending the event. + * 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?: string + insert_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 + * The timestamp of the event. If time is not sent with the event, it will be set to the request upload time. */ - userAgentParsing?: boolean + time?: string | number /** - * Enabling this setting will send user_agent based on the raw user agent string provided in the userAgent field + * 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. */ - includeRawUserAgent?: boolean + min_id_length?: number | null /** * UTM Tracking Properties */ @@ -211,15 +244,4 @@ export interface Payload { * 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 lenght 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 7db7dc8889e..44de5f29be8 100644 --- a/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/logPurchase/index.ts @@ -1,308 +1,39 @@ -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 { send } from '../events-functions' +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, time, device_id, insert_id, utm_properties, referrer} from '../fields/misc-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 - }, - ...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' - } - } - ] - } - }, - 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 - }, - 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 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' - }, - userAgentData + trackRevenuePerProduct, + ...common_fields, + ...common_track_fields, + ...common_track_identify_fields, + ...autocapture_fields, + device_id, + insert_id, + time, + min_id_length, + utm_properties, + referrer }, 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 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/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 } diff --git a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts index 88f4cc3ea0e..60a54f91bc1 100644 --- a/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts +++ b/packages/destination-actions/src/destinations/amplitude/mapUser/index.ts @@ -1,7 +1,10 @@ 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' +import { user_id } from '../fields/common-fields' +import { global_user_id } from './fields' +import { min_id_length } from '../fields/misc-fields' const action: ActionDefinition = { title: 'Map User', @@ -9,28 +12,13 @@ const action: ActionDefinition = { defaultSubscription: 'type = "alias"', fields: { user_id: { - label: 'User ID', - type: 'string', - description: 'The User ID to be associated.', + ...user_id, default: { '@path': '$.previousId' } }, - global_user_id: { - label: 'Global User ID', - type: 'string', - description: 'The Global User ID to associate with the User ID.', - default: { - '@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' - } + global_user_id, + min_id_length }, perform: (request, { payload, settings }) => { const { min_id_length } = payload @@ -46,4 +34,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/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/types.ts b/packages/destination-actions/src/destinations/amplitude/types.ts new file mode 100644 index 00000000000..547ad6522c3 --- /dev/null +++ b/packages/destination-actions/src/destinations/amplitude/types.ts @@ -0,0 +1,71 @@ +export type Region = 'north_america' | 'europe' + +export interface AmplitudeEventJSON extends EventRevenue { + 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 + device_brand?: string + carrier?: string + country?: string + region?: string + city?: string + dma?: string + language?: 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 + 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 { + revenue?: number + price?: number + productId?: string + quantity?: number + revenueType?: string +} + +export interface UserProperties { + $set?: Record + $setOnce?: Record<`initial_${string}`, string> + $unset?: Record + $add?: Record + [k: string]: unknown +} + +export interface JSON_PAYLOAD { + api_key: string + 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 b4c13a4a3a6..00000000000 --- a/packages/destination-actions/src/destinations/amplitude/user-agent.ts +++ /dev/null @@ -1,37 +0,0 @@ -import UaParser from '@amplitude/ua-parser-js' - -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 - } -}